mod count;
mod dir_references;
mod directive;
mod duplicates;
mod file_references;
mod path_util;
mod tag_references;
mod walk;
use clap::{ArgAction, Args, Parser, Subcommand as ClapSubcommand};
use colored::Colorize;
use directive::compile_directive_regex;
use std::{
collections::{HashMap, HashSet},
io::{self, BufReader, IsTerminal},
path::PathBuf,
process::exit,
sync::{Arc, Mutex},
};
#[derive(Parser)]
#[command(
about = concat!(
env!("CARGO_PKG_DESCRIPTION"),
"\n\n",
"You can annotate your code with tags like [tag:foo] and reference them like [ref:foo]. ",
"You can also reference files like [file:src/main.rs] and directories like [dir:src]. ",
"Tagref checks that tags are unique and that references are not dangling.\n\n",
"More information can be found at: ",
env!("CARGO_PKG_HOMEPAGE"),
),
version,
disable_version_flag = true
)]
struct Cli {
#[arg(short, long, help = "Print version", action = ArgAction::Version)]
_version: Option<bool>,
#[arg(
short,
long = "path",
value_name = "PATH",
help = "Add a directory to scan",
default_value = "."
)]
paths: Vec<PathBuf>,
#[arg(
short,
long,
help = "Set the sigil used for tags",
default_value = "tag"
)]
tag_sigil: String,
#[arg(
short,
long,
help = "Set the sigil used for tag references",
default_value = "ref"
)]
ref_sigil: String,
#[arg(
short,
long,
help = "Set the sigil used for file references",
default_value = "file"
)]
file_sigil: String,
#[arg(
short,
long,
help = "Set the sigil used for directory references",
default_value = "dir"
)]
dir_sigil: String,
#[command(subcommand)]
command: Option<Subcommand>,
}
#[derive(Args)]
struct ListUnusedArgs {
#[arg(
long,
help = "Exit with an error status code if any tags are unreferenced"
)]
fail_if_any: bool,
}
#[derive(ClapSubcommand)]
enum Subcommand {
#[command(about = "Check all the tags and references (default)")]
Check,
#[command(about = "List all the tags")]
ListTags,
#[command(about = "List all the tag references")]
ListRefs,
#[command(about = "List all the file references")]
ListFiles,
#[command(about = "List all the directory references")]
ListDirs,
#[command(about = "List the unreferenced tags")]
ListUnused(ListUnusedArgs),
}
#[allow(clippy::too_many_lines)]
fn entry() -> Result<(), String> {
colored::control::set_override(io::stdout().is_terminal());
let cli = Cli::parse();
let tag_regex = compile_directive_regex(&cli.tag_sigil);
let ref_regex = compile_directive_regex(&cli.ref_sigil);
let file_regex = compile_directive_regex(&cli.file_sigil);
let dir_regex = compile_directive_regex(&cli.dir_sigil);
let tags = Arc::new(Mutex::new(HashMap::new()));
let refs = Arc::new(Mutex::new(Vec::new()));
let files = Arc::new(Mutex::new(Vec::new()));
let dirs = Arc::new(Mutex::new(Vec::new()));
let tags_clone = tags.clone();
let refs_clone = refs.clone();
let files_clone = files.clone();
let dirs_clone = dirs.clone();
let tag_regex_clone = tag_regex.clone();
let ref_regex_clone = ref_regex.clone();
let file_regex_clone = file_regex.clone();
let dir_regex_clone = dir_regex.clone();
let files_scanned = walk::walk(&cli.paths, move |file_path, file| {
let directives = directive::parse(
&tag_regex_clone,
&ref_regex_clone,
&file_regex_clone,
&dir_regex_clone,
file_path,
BufReader::new(file),
);
for tag in directives.tags {
tags_clone
.lock()
.unwrap() .entry(tag.label.clone())
.or_insert_with(Vec::new)
.push(tag.clone());
}
refs_clone.lock().unwrap().extend(directives.refs); files_clone.lock().unwrap().extend(directives.files); dirs_clone.lock().unwrap().extend(directives.dirs); });
match cli.command.unwrap_or(Subcommand::Check) {
Subcommand::Check => {
let mut errors = Vec::<String>::new();
errors.extend(duplicates::check(&tags.lock().unwrap()));
let tags = tags
.lock()
.unwrap()
.keys()
.cloned()
.collect::<HashSet<String>>();
let refs = refs.lock().unwrap();
errors.extend(tag_references::check(&tags, &refs));
errors.extend(file_references::check(&files.lock().unwrap()));
errors.extend(dir_references::check(&dirs.lock().unwrap()));
if errors.is_empty() {
println!(
"{}",
format!(
"{}, {}, {}, and {} validated in {}.",
count::count(tags.len(), "tag"),
count::count(refs.len(), "tag reference"),
count::count(files.lock().unwrap().len(), "file reference"),
count::count(dirs.lock().unwrap().len(), "directory reference"),
count::count(files_scanned, "file"),
)
.green(),
);
} else {
return Err(errors.join("\n\n"));
}
}
Subcommand::ListTags => {
for dupes in tags.lock().unwrap().values() {
for dupe in dupes {
println!("{dupe}");
}
}
}
Subcommand::ListRefs => {
for r#ref in refs.lock().unwrap().iter() {
println!("{ref}");
}
}
Subcommand::ListFiles => {
for file in files.lock().unwrap().iter() {
println!("{file}");
}
}
Subcommand::ListDirs => {
for dir in dirs.lock().unwrap().iter() {
println!("{dir}");
}
}
Subcommand::ListUnused(args) => {
for r#ref in refs.lock().unwrap().iter() {
tags.lock()
.unwrap() .remove(&r#ref.label);
}
for dupes in tags.lock().unwrap().values() {
for dupe in dupes {
println!("{dupe}");
}
}
if args.fail_if_any && !tags.lock().unwrap().is_empty() {
return Err("Found unused tags while using --fail-if-any".to_owned());
}
}
}
Ok(())
}
fn main() {
if let Err(e) = entry() {
eprintln!("{}", e.red());
exit(1);
}
}
#[cfg(test)]
mod tests {
use super::Cli;
use clap::CommandFactory;
#[test]
fn verify_cli() {
Cli::command().debug_assert();
}
}