rusty-tags 0.6.4

Create ctags/etags for a cargo project and all of its dependencies
use std::fs::{File, OpenOptions};
use std::io::{Read, Write};
use std::process::Command;
use std::collections::HashSet;
use std::path::{PathBuf, Path};

use app_result::{AppResult, app_err_msg, app_err_missing_src};
use types::{Tags, TagsKind, SourceKind};
use path_ext::PathExt;
use config::Config;

use dirs::{
   rusty_tags_cache_dir,
   cargo_git_src_dir,
   cargo_crates_io_src_dir,
   glob_path
};

/// Checks if there's already a tags file for `source`
/// and if not it's creating a new tags file and returning it.
pub fn update_tags(config: &Config, source: &SourceKind) -> AppResult<Tags>
{
   let src_tags = try!(cached_tags_file(&config.tags_kind, source));
   let src_dir = try!(find_src_dir(source));
   if src_tags.as_path().is_file() && ! config.force_recreate {
      return Ok(Tags::new(&src_dir, &src_tags, true));
   }

   try!(create_tags(config, &src_dir, &src_tags));
   Ok(Tags::new(&src_dir, &src_tags, false))
}

/// Does the same thing as `update_tags`, but also checks if the `lib.rs`
/// file of the library has public reexports of external crates. If
/// that's the case, then the tags of the public reexported external
/// crates are merged into the tags of the library.
pub fn update_tags_and_check_for_reexports(config: &Config,
                                           source: &SourceKind,
                                           dependencies: &Vec<SourceKind>) -> AppResult<Tags>
{
   let lib_tags = try!(update_tags(config, source));
   if lib_tags.is_up_to_date(&config.tags_kind) && ! config.force_recreate {
      return Ok(lib_tags);
   }

   let reexp_crates = try!(find_reexported_crates(&lib_tags.src_dir));
   if reexp_crates.is_empty() {
      return Ok(lib_tags);
   }

   println!("Found public reexports in '{}' of:", source.get_lib_name());
   for rcrate in reexp_crates.iter() {
      println!("   {}", rcrate);
   }
   println!("");

   let mut crate_tags = Vec::<PathBuf>::new();
   for rcrate in reexp_crates.iter() {
      if let Some(crate_dep) = dependencies.iter().find(|d| d.get_lib_name() == *rcrate) {
         crate_tags.push(try!(update_tags(config, crate_dep)).tags_file.clone());
      }
   }

   if crate_tags.is_empty() {
      return Ok(lib_tags);
   }

   crate_tags.push(lib_tags.tags_file.clone());
   try!(merge_tags(config, &crate_tags, &lib_tags.tags_file));
   Ok(lib_tags)
}

/// merges `tag_files` into `into_tag_file`
pub fn merge_tags(config: &Config, tag_files: &Vec<PathBuf>, into_tag_file: &Path) -> AppResult<()>
{
   println!("Merging ...\n   tags:");

   for file in tag_files.iter() {
      println!("      {}", file.display());
   }

   println!("\n   into:\n      {}\n", into_tag_file.display());

   match config.tags_kind {
      TagsKind::Vi => {
         let mut file_contents: Vec<String> = Vec::new();
         for file in tag_files.iter() {
            let mut file = try!(File::open(file));
            let mut contents = String::new();
            try!(file.read_to_string(&mut contents));
            file_contents.push(contents);
         }

         let mut merged_lines: Vec<&str> = Vec::with_capacity(100_000);
         for content in file_contents.iter() {
            for line in content.lines() {
               if let Some(chr) = line.chars().nth(0) {
                  if chr != '!' {
                     merged_lines.push(line);
                  }
               }
            }
         }

         merged_lines.sort();
         merged_lines.dedup();

         let mut tag_file = try!(
             OpenOptions::new()
                .create(true)
                .truncate(true)
                .read(true)
                .write(true)
                .open(into_tag_file)
         );

         try!(tag_file.write_fmt(format_args!("{}\n", "!_TAG_FILE_FORMAT	2	/extended format; --format=1 will not append ;\" to lines/")));
         try!(tag_file.write_fmt(format_args!("{}\n", "!_TAG_FILE_SORTED	1	/0=unsorted, 1=sorted, 2=foldcase/")));

         for line in merged_lines.iter() {
            try!(tag_file.write_fmt(format_args!("{}\n", *line)));
         }
      },

      TagsKind::Emacs => {
         let mut tag_file = try!(
             OpenOptions::new()
                .create(true)
                .append(true)
                .read(true)
                .write(true)
                .open(into_tag_file)
         );

         for file in tag_files.iter() {
            if file.as_path() != into_tag_file {
               try!(tag_file.write_fmt(format_args!("{},include\n", file.display())));
            }
         }
      }
   }

   Ok(())
}

/// creates tags recursive for the directory hierarchy starting at `src_dir`
/// and writes them to `tags_file`
pub fn create_tags(config: &Config, src_dir: &Path, tags_file: &Path) -> AppResult<()>
{
   let mut cmd = Command::new("ctags");

   config.tags_kind.ctags_option().map(|opt| { cmd.arg(opt); () });

   cmd.arg("--recurse")
      .arg("--languages=Rust")
      .arg("--langdef=Rust")
      .arg("--langmap=Rust:.rs")
      .arg("--regex-Rust=/^[ \\t]*(#\\[[^\\]]\\][ \\t]*)*(pub[ \\t]+)?(extern[ \\t]+)?(\"[^\"]+\"[ \\t]+)?(unsafe[ \\t]+)?fn[ \\t]+([a-zA-Z0-9_]+)/\\6/f,functions,function definitions/")
      .arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?type[ \\t]+([a-zA-Z0-9_]+)/\\2/T,types,type definitions/")
      .arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?enum[ \\t]+([a-zA-Z0-9_]+)/\\2/g,enum,enumeration names/")
      .arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?struct[ \\t]+([a-zA-Z0-9_]+)/\\2/s,structure names/")
      .arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?mod[ \\t]+([a-zA-Z0-9_]+)/\\2/m,modules,module names/")
      .arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?(static|const)[ \\t]+([a-zA-Z0-9_]+)/\\3/c,consts,static constants/")
      .arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?trait[ \\t]+([a-zA-Z0-9_]+)/\\2/t,traits,traits/")
      .arg("--regex-Rust=/^[ \\t]*(pub[ \\t]+)?impl([ \\t\\n]*<[^>]*>)?[ \\t]+(([a-zA-Z0-9_:]+)[ \\t]*(<[^>]*>)?[ \\t]+(for)[ \\t]+)?([a-zA-Z0-9_]+)/\\4 \\6 \\7/i,impls,trait implementations/")
      .arg("--regex-Rust=/^[ \\t]*macro_rules![ \\t]+([a-zA-Z0-9_]+)/\\1/d,macros,macro definitions/")
      .arg("-o")
      .arg(tags_file)
      .arg(src_dir);

   println!("Creating tags ...\n   for source:\n      {}\n\n   cached at:\n      {}\n",
            src_dir.display(), tags_file.display());

   try!(cmd.output());
   Ok(())
}

/// find the source directory of `source`, for git sources the directories
/// in `~/.cargo/git/checkouts` are considered and for crates.io sources
/// the directories in `~/.cargo/registry/src/github.com-*` are considered
fn find_src_dir(source: &SourceKind) -> AppResult<PathBuf>
{
   match *source {
      SourceKind::Git { ref lib_name, ref commit_hash } => {
         let mut lib_src = lib_name.clone();
         lib_src.push_str("-*");

         let mut src_dir = try!(cargo_git_src_dir().map(Path::to_path_buf));
         src_dir.push(&lib_src);
         src_dir.push("master");

         let src_paths = try!(glob_path(&format!("{}", src_dir.display())));
         for src_path in src_paths {
            if let Ok(path) = src_path {
               let src_commit_hash = try!(get_commit_hash(&path));
               if *commit_hash == src_commit_hash {
                  return Ok(path);
               }
            }
         }

         // the git repository name hasn't to match the name of the library,
         // so here we're just going through all git directories and searching
         // for the one with a matching commit hash
         let mut src_dir = try!(cargo_git_src_dir().map(Path::to_path_buf));
         src_dir.push("*");
         src_dir.push("master");

         let src_paths = try!(glob_path(&format!("{}", src_dir.display())));
         for src_path in src_paths {
            if let Ok(path) = src_path {
               let src_commit_hash = try!(get_commit_hash(&path));
               if *commit_hash == src_commit_hash {
                  return Ok(path);
               }
            }
         }

         Err(app_err_missing_src(source))
      },

      SourceKind::CratesIo { ref lib_name, ref version } => {
         let mut lib_src = lib_name.clone();
         lib_src.push('-');
         lib_src.push_str(&**version);

         let mut src_dir = try!(cargo_crates_io_src_dir().map(Path::to_path_buf));
         src_dir.push(&lib_src);

         if ! src_dir.is_dir() {
            return Err(app_err_missing_src(source));
         }

         Ok(src_dir)
      },

      SourceKind::Path { ref path, .. } => {
         if ! path.is_dir() {
            return Err(app_err_missing_src(source));
         }

         Ok(path.clone())
      }
   }
}

/// returns the position and name of the cached tags file of `source`
fn cached_tags_file(tags_kind: &TagsKind, source: &SourceKind) -> AppResult<PathBuf>
{
   match *source {
      SourceKind::Git { .. } | SourceKind::CratesIo { .. } => {
         let mut tags_file = try!(rusty_tags_cache_dir().map(Path::to_path_buf));
         tags_file.push(&source.tags_file_name(tags_kind));
         Ok(tags_file)
      },

      SourceKind::Path { ref path, .. } => {
         let mut tags_file = path.clone();
         tags_file.push(&source.tags_file_name(tags_kind));
         Ok(tags_file)
      }
   }
}

type CrateName = String;

/// searches in the file `<src_dir>/src/lib.rs` for external crates
/// that are reexpored and returns their names
fn find_reexported_crates(src_dir: &Path) -> AppResult<Vec<CrateName>>
{
   let mut lib_file = src_dir.to_path_buf();
   lib_file.push("src");
   lib_file.push("lib.rs");

   if ! lib_file.is_file() {
      return Ok(Vec::new());
   }

   let contents = {
      let mut file = try!(File::open(&lib_file));
      let mut contents = String::new();
      try!(file.read_to_string(&mut contents));
      contents
   };

   let lines = contents.lines();

   type ModuleName = String;
   let mut pub_uses = HashSet::<ModuleName>::new();

   #[derive(Eq, PartialEq, Hash)]
   struct ExternCrate<'a>
   {
      name: &'a str,
      as_name: &'a str
   }

   let mut extern_crates = HashSet::<ExternCrate>::new();

   for line in lines {
      let items = line.trim_matches(';').split(' ').collect::<Vec<&str>>();
      if items.len() < 3 {
         continue;
      }

      if items[0] == "pub" && items[1] == "use" {
         let mods = items[2].split("::").collect::<Vec<&str>>();
         if mods.len() >= 1 {
            pub_uses.insert(mods[0].to_string());
         }
      }

      if items[0] == "extern" && items[1] == "crate" {
         if items.len() == 3 {
            extern_crates.insert(ExternCrate { name: items[2].trim_matches('"'), as_name: items[2] });
         }
         else if items.len() == 5 && items[3] == "as" {
            extern_crates.insert(ExternCrate { name: items[2].trim_matches('"'), as_name: items[4] });
         }
      }
   }

   let mut reexp_crates = Vec::<CrateName>::new();
   for extern_crate in extern_crates.iter() {
      if pub_uses.contains(extern_crate.as_name) {
         reexp_crates.push(extern_crate.name.to_string());
      }
   }

   Ok(reexp_crates)
}

/// get the commit hash of the current `HEAD` of the git repository located at `git_dir`
fn get_commit_hash(git_dir: &Path) -> AppResult<String>
{
   let mut cmd = Command::new("git");
   cmd.current_dir(git_dir)
      .arg("rev-parse")
      .arg("HEAD");

   let out = try!(cmd.output());
   String::from_utf8(out.stdout)
      .map(|s| s.trim().to_string())
      .map_err(|_| app_err_msg("Couldn't convert 'git rev-parse HEAD' output to utf8!".to_string()))
}