#![deny(warnings)]
mod count;
mod duplicates;
mod label;
mod references;
mod walk;
use atty::Stream;
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
use colored::Colorize;
use regex::{escape, Regex};
use std::{
collections::HashMap,
io::BufReader,
path::{Path, PathBuf},
process::exit,
sync::{Arc, Mutex},
};
const VERSION: &str = env!("CARGO_PKG_VERSION");
const CHECK_SUBCOMMAND: &str = "check";
const LIST_REFS_SUBCOMMAND: &str = "list-refs";
const LIST_TAGS_SUBCOMMAND: &str = "list-tags";
const LIST_UNUSED_SUBCOMMAND: &str = "list-unused";
const PATH_OPTION: &str = "path";
const TAG_PREFIX_OPTION: &str = "tag-prefix";
const REF_PREFIX_OPTION: &str = "ref-prefix";
fn settings<'a>() -> (ArgMatches<'a>, Vec<PathBuf>, String, String) {
let matches = App::new("Tagref")
.version(VERSION)
.version_short("v")
.author("Stephan Boyer <stephan@stephanboyer.com>")
.about(
" \
Tagref helps you refer to other locations in your codebase. It checks \
the following:\n\n1. References actually point to tags. \n2. Tags are \
distinct.\n\nThe syntax for tags is [tag:?label?] and the syntax for \
references is [ref:?label?]. For more information, visit \
https://github.com/stepchowfun/tagref. \
"
.replace("?", "") .trim(),
)
.setting(AppSettings::ColoredHelp)
.setting(AppSettings::NextLineHelp)
.setting(AppSettings::UnifiedHelpMessage)
.setting(AppSettings::VersionlessSubcommands)
.arg(
Arg::with_name(PATH_OPTION)
.value_name("PATH")
.short("p")
.long(PATH_OPTION)
.help("Adds the path of a directory to scan")
.default_value(".") .multiple(true),
)
.arg(
Arg::with_name(TAG_PREFIX_OPTION)
.value_name("TAG_PREFIX")
.short("t")
.long(TAG_PREFIX_OPTION)
.help("Sets the prefix used for locating tags")
.default_value("tag"), )
.arg(
Arg::with_name(REF_PREFIX_OPTION)
.value_name("REF_PREFIX")
.short("r")
.long(REF_PREFIX_OPTION)
.help("Sets the prefix used for locating references")
.default_value("ref"), )
.subcommand(
SubCommand::with_name(CHECK_SUBCOMMAND)
.about("Checks all the tags and references (default)"),
)
.subcommand(SubCommand::with_name(LIST_TAGS_SUBCOMMAND).about("Lists all the tags"))
.subcommand(
SubCommand::with_name(LIST_UNUSED_SUBCOMMAND).about("Lists the unreferenced tags"),
)
.subcommand(SubCommand::with_name(LIST_REFS_SUBCOMMAND).about("Lists all the references"))
.get_matches();
let paths = matches
.values_of(PATH_OPTION)
.unwrap()
.map(|path| Path::new(path).to_owned())
.collect::<Vec<_>>();
let ref_prefix = matches.value_of(REF_PREFIX_OPTION).unwrap().to_owned();
let tag_prefix = matches.value_of(TAG_PREFIX_OPTION).unwrap().to_owned();
(matches, paths, tag_prefix, ref_prefix)
}
#[allow(clippy::too_many_lines)]
fn entry() -> Result<(), String> {
colored::control::set_override(atty::is(Stream::Stdout));
let (matches, paths, tag_prefix, ref_prefix) = settings();
let tag_regex: Regex = Regex::new(&format!(
"(?i)\\[\\s*{}\\s*:\\s*([^\\]\\s]*)\\s*\\]",
escape(&tag_prefix),
))
.unwrap();
let ref_regex: Regex = Regex::new(&format!(
"(?i)\\[\\s*{}\\s*:\\s*([^\\]\\s]*)\\s*\\]",
escape(&ref_prefix),
))
.unwrap();
match matches.subcommand_name() {
Some(LIST_TAGS_SUBCOMMAND) => {
let mutex = Arc::new(Mutex::new(()));
let _ = walk::walk(&paths, move |file_path, file| {
for tag in label::parse(
&tag_regex,
&ref_regex,
label::Type::Tag,
file_path,
BufReader::new(file),
) {
let _lock = mutex.lock().unwrap(); println!("{}", tag);
}
});
}
Some(LIST_REFS_SUBCOMMAND) => {
let mutex = Arc::new(Mutex::new(()));
let _ = walk::walk(&paths, move |file_path, file| {
for r#ref in label::parse(
&tag_regex,
&ref_regex,
label::Type::Ref,
file_path,
BufReader::new(file),
) {
let _lock = mutex.lock().unwrap(); println!("{}", r#ref);
}
});
}
Some(LIST_UNUSED_SUBCOMMAND) => {
let tags_map = Arc::new(Mutex::new(HashMap::new()));
let tags_map_add_tags = tags_map.clone();
let tag_regex_clone = tag_regex.clone();
let ref_regex_clone = tag_regex.clone();
let _ = walk::walk(&paths, move |file_path, file| {
for tag in label::parse(
&tag_regex_clone,
&ref_regex_clone,
label::Type::Tag,
file_path,
BufReader::new(file),
) {
tags_map_add_tags
.lock()
.unwrap() .entry(tag.label.clone())
.or_insert_with(Vec::new)
.push(tag.clone());
}
});
let tags_map_remove_tags = tags_map.clone();
let _ = walk::walk(&paths, move |file_path, file| {
for r#ref in label::parse(
&tag_regex,
&ref_regex,
label::Type::Ref,
file_path,
BufReader::new(file),
) {
tags_map_remove_tags
.lock()
.unwrap() .remove(&r#ref.label);
}
});
for tags in tags_map.lock().unwrap().values() {
for tag in tags {
println!("{}", tag);
}
}
}
Some(CHECK_SUBCOMMAND) | None => {
let tags = Arc::new(Mutex::new(HashMap::new()));
let tags_clone = tags.clone();
let tag_regex_clone = tag_regex.clone();
let ref_regex_clone = tag_regex.clone();
let _ = walk::walk(&paths, move |file_path, file| {
for tag in label::parse(
&tag_regex_clone,
&ref_regex_clone,
label::Type::Tag,
file_path,
BufReader::new(file),
) {
tags_clone
.lock()
.unwrap() .entry(tag.label.clone())
.or_insert_with(Vec::new)
.push(tag.clone());
}
});
let tags = duplicates::check(&tags.lock().unwrap())?;
let refs = Arc::new(Mutex::new(Vec::new()));
let refs_clone = refs.clone();
let files_scanned = walk::walk(&paths, move |file_path, file| {
let results = label::parse(
&tag_regex,
&ref_regex,
label::Type::Ref,
file_path,
BufReader::new(file),
);
if !results.is_empty() {
refs_clone
.lock()
.unwrap() .extend(results);
}
});
let refs = refs.lock().unwrap();
references::check(&tags, &refs)?;
println!(
"{}",
format!(
"{} and {} validated in {}.",
count::count(tags.len(), "tag"),
count::count(refs.len(), "reference"),
count::count(files_scanned, "file"),
)
.green(),
);
}
Some(&_) => panic!(),
}
Ok(())
}
fn main() {
if let Err(e) = entry() {
eprintln!("{}", e.red());
exit(1);
}
}