extern crate clap;
#[macro_use]
extern crate log;
extern crate rayon;
extern crate simplelog;
extern crate walkdir;
use colored::Colorize;
use rayon::iter::ParallelBridge;
use rayon::prelude::*;
use regex::Regex;
use std::sync::mpsc::{Receiver, Sender};
use std::sync::{mpsc, Arc};
use std::{collections::HashMap, path::Path};
use std::{collections::HashSet, fs};
use std::{io::prelude::*, path::PathBuf};
use tree_sitter::Tree;
use walkdir::WalkDir;
use weggli::RegexMap;
use weggli::builder::build_query_tree;
use weggli::query::QueryTree;
use weggli::result::QueryResult;
mod cli;
fn main() {
reset_signal_pipe_handler();
let args = cli::parse_arguments();
if args.force_color {
colored::control::set_override(true)
}
let mut variables = HashSet::new();
let regex_constraints = process_regexes(&args.regexes).unwrap_or_else(|e| {
let msg = match e {
RegexError::InvalidArg(s) => format!(
"'{}' is not a valid argument of the form var=regex",
s.red()
),
RegexError::InvalidRegex(s) => format!("Regex error {}", s),
};
eprintln!("{}", msg);
std::process::exit(1)
});
let work: Vec<WorkItem> = args
.pattern
.iter()
.map(|pattern| {
let qt = parse_search_pattern(pattern, args.cpp, args.force_query, ®ex_constraints);
let identifiers = qt.identifiers();
variables.extend(qt.variables());
WorkItem { qt, identifiers }
})
.collect();
for v in regex_constraints.variables() {
if !variables.contains(v) {
eprintln!("'{}' is not a valid query variable", v.red());
std::process::exit(1)
}
}
let helper_regex = |v: &[String]| -> Vec<Regex> {
v.iter()
.map(|s| {
let r = Regex::new(s);
match r {
Ok(regex) => regex,
Err(e) => {
eprintln!("Regex error {}", e);
std::process::exit(1)
}
}
})
.collect()
};
let exclude_re = helper_regex(&args.exclude);
let include_re = helper_regex(&args.include);
let mut files: Vec<PathBuf> = if args.path.to_string_lossy() == "-" {
std::io::stdin()
.lock()
.lines()
.filter_map(|l| l.ok())
.map(|s| Path::new(&s).to_path_buf())
.collect()
} else {
iter_files(&args.path, args.extensions.clone())
.map(|d| d.into_path())
.collect()
};
if !exclude_re.is_empty() || !include_re.is_empty() {
files.retain(|f| {
if exclude_re.iter().any(|r| r.is_match(&f.to_string_lossy())) {
return false;
}
if include_re.is_empty() {
return true;
}
include_re.iter().any(|r| r.is_match(&f.to_string_lossy()))
});
}
info!("parsing {} files", files.len());
if files.is_empty() {
eprintln!("{}", String::from("No files to parse. Exiting...").red());
std::process::exit(1)
}
rayon::scope(|s| {
let (ast_tx, ast_rx) = mpsc::channel();
let (results_tx, results_rx) = mpsc::channel();
let cpp = args.cpp;
let w = &work;
let before = args.before;
let after = args.after;
s.spawn(move |_| parse_files_worker(files, ast_tx, w, cpp));
s.spawn(move |_| execute_queries_worker(ast_rx, results_tx, w, &args));
if w.len() > 1 {
s.spawn(move |_| multi_query_worker(results_rx, w.len(), before, after));
}
});
}
const VALID_NODE_KINDS: &[&str] = &[
"compound_statement",
"function_definition",
"struct_specifier",
"enum_specifier",
"union_specifier",
"class_specifier",
];
fn parse_search_pattern(
pattern: &str,
is_cpp: bool,
force_query: bool,
regex_constraints: &RegexMap,
) -> QueryTree {
let mut tree = weggli::parse(pattern, is_cpp);
let mut p = pattern;
let temp_pattern;
if tree.root_node().has_error() {
if !pattern.ends_with(';') {
temp_pattern = format!("{};", &p);
let fixed_tree = weggli::parse(&temp_pattern, is_cpp);
if !fixed_tree.root_node().has_error() {
info!("normalizing query: add missing ;");
tree = fixed_tree;
p = &temp_pattern;
}
}
}
let temp_pattern2;
if !tree.root_node().has_error() {
let c = tree.root_node().child(0);
if let Some(n) = c {
if !VALID_NODE_KINDS.contains(&n.kind()) {
temp_pattern2 = format!("{{{}}}", &p);
let fixed_tree = weggli::parse(&temp_pattern2, is_cpp);
if !fixed_tree.root_node().has_error() {
info!("normalizing query: add {}", "{}");
tree = fixed_tree;
p = &temp_pattern2;
}
}
}
}
let mut c = validate_query(&tree, p, force_query);
build_query_tree(p, &mut c, is_cpp, Some(regex_constraints.clone()))
}
fn validate_query<'a>(
tree: &'a tree_sitter::Tree,
query: &str,
force: bool,
) -> tree_sitter::TreeCursor<'a> {
if tree.root_node().has_error() && !force {
eprint!("{}", "Error! Query parsing failed:".red().bold());
let mut cursor = tree.root_node().walk();
let mut first_error = None;
loop {
let node = cursor.node();
if node.has_error() {
if node.is_error() || node.is_missing() {
first_error = Some(node);
break;
} else if !cursor.goto_first_child() {
break;
}
} else if !cursor.goto_next_sibling() {
break;
}
}
if let Some(node) = first_error {
eprint!(" {}", &query[0..node.start_byte()].italic());
if node.is_missing() {
eprint!(
"{}{}{}",
" [MISSING ".red(),
node.kind().red().bold(),
" ] ".red()
);
}
eprint!(
"{}",
&query[node.start_byte()..node.end_byte()]
.red()
.italic()
.bold()
);
eprintln!("{}", &query[node.end_byte()..].italic());
}
std::process::exit(1);
}
info!("query sexp: {}", tree.root_node().to_sexp());
let mut c = tree.walk();
if c.node().named_child_count() > 1 {
eprintln!(
"{}'{}' query contains multiple root nodes",
"Error: ".red(),
query
);
std::process::exit(1);
}
c.goto_first_child();
if !VALID_NODE_KINDS.contains(&c.node().kind()) {
eprintln!(
"{}'{}' is not a supported query root node.",
"Error: ".red(),
query
);
std::process::exit(1);
}
c
}
enum RegexError {
InvalidArg(String),
InvalidRegex(regex::Error),
}
impl From<regex::Error> for RegexError {
fn from(err: regex::Error) -> RegexError {
RegexError::InvalidRegex(err)
}
}
fn process_regexes(regexes: &[String]) -> Result<RegexMap, RegexError> {
let mut result = HashMap::new();
for r in regexes {
let mut s = r.splitn(2, '=');
let var = s.next().ok_or_else(|| RegexError::InvalidArg(r.clone()))?;
let raw_regex = s.next().ok_or_else(|| RegexError::InvalidArg(r.clone()))?;
let mut normalized_var = if var.starts_with('$') {
var.to_string()
} else {
"$".to_string() + var
};
let negative = normalized_var.ends_with('!');
if negative {
normalized_var.pop(); }
let regex = Regex::new(raw_regex)?;
result.insert(normalized_var, (negative, regex));
}
Ok(RegexMap::new(result))
}
fn iter_files(path: &Path, extensions: Vec<String>) -> impl Iterator<Item = walkdir::DirEntry> {
let is_hidden = |entry: &walkdir::DirEntry| {
entry
.file_name()
.to_str()
.map(|s| s.starts_with('.'))
.unwrap_or(false)
};
WalkDir::new(path)
.into_iter()
.filter_entry(move |e| !is_hidden(e))
.filter_map(|e| e.ok())
.filter(move |entry| {
if entry.file_type().is_dir() {
return false;
}
let path = entry.path();
match path.extension() {
None => return false,
Some(ext) => {
let s = ext.to_str().unwrap_or_default();
if !extensions.contains(&s.to_string()) {
return false;
}
}
}
true
})
}
struct WorkItem {
qt: QueryTree,
identifiers: Vec<String>,
}
fn parse_files_worker(
files: Vec<PathBuf>,
sender: Sender<(Arc<String>, Tree, String)>,
work: &[WorkItem],
is_cpp: bool,
) {
files
.into_par_iter()
.for_each_with(sender, move |sender, path| {
let maybe_parse = |path| {
let c = match fs::read(path) {
Ok(content) => content,
Err(_) => return None,
};
let source = String::from_utf8_lossy(&c);
let potential_match = work.iter().any(|WorkItem { qt: _, identifiers }| {
identifiers.iter().all(|i| source.find(i).is_some())
});
if !potential_match {
None
} else {
Some((weggli::parse(&source, is_cpp), source.to_string()))
}
};
if let Some((source_tree, source)) = maybe_parse(&path) {
sender
.send((
std::sync::Arc::new(source),
source_tree,
path.display().to_string(),
))
.unwrap();
}
});
}
struct ResultsCtx {
query_index: usize,
path: String,
source: std::sync::Arc<String>,
result: weggli::result::QueryResult,
}
fn execute_queries_worker(
receiver: Receiver<(Arc<String>, Tree, String)>,
results_tx: Sender<ResultsCtx>,
work: &[WorkItem],
args: &cli::Args,
) {
receiver.into_iter().par_bridge().for_each_with(
results_tx,
|results_tx, (source, tree, path)| {
work.iter()
.enumerate()
.for_each(|(i, WorkItem { qt, identifiers: _ })| {
let matches = qt.matches(tree.root_node(), &source);
if matches.is_empty() {
return;
}
let check_unique = |m: &QueryResult| {
if args.unique {
let mut seen = HashSet::new();
m.vars
.keys()
.map(|k| m.value(k, &source).unwrap())
.all(|x| seen.insert(x))
} else {
true
}
};
let mut skip_set = HashSet::new();
let check_limit = |m: &QueryResult| {
if args.limit {
skip_set.insert(m.start_offset())
} else {
true
}
};
let process_match = |m: QueryResult| {
if work.len() == 1 {
let line = source[..m.start_offset()].matches('\n').count() + 1;
println!(
"{}:{}\n{}",
path.clone().bold(),
line,
m.display(&source, args.before, args.after)
);
} else {
results_tx
.send(ResultsCtx {
query_index: i,
result: m,
path: path.clone(),
source: source.clone(),
})
.unwrap();
}
};
matches
.into_iter()
.filter(check_unique)
.filter(check_limit)
.for_each(process_match);
});
},
);
}
fn multi_query_worker(
results_rx: Receiver<ResultsCtx>,
num_queries: usize,
before: usize,
after: usize,
) {
let mut query_results = Vec::with_capacity(num_queries);
for _ in 0..num_queries {
query_results.push(Vec::new());
}
for ctx in results_rx {
query_results[ctx.query_index].push(ctx);
}
let filter = |x: &mut Vec<ResultsCtx>, y: &mut Vec<ResultsCtx>| {
x.retain(|r| {
y.iter()
.any(|f| r.result.chainable(&r.source, &f.result, &f.source))
})
};
for i in 0..query_results.len() {
let (part1, part2) = query_results.split_at_mut(i + 1);
let a = part1.last_mut().unwrap();
for b in part2 {
filter(a, b);
filter(b, a);
}
}
query_results.into_iter().for_each(|rv| {
rv.into_iter().for_each(|r| {
let line = r.source[..r.result.start_offset()].matches('\n').count() + 1;
println!(
"{}:{}\n{}",
r.path.bold(),
line,
r.result.display(&r.source, before, after)
);
})
});
}
fn reset_signal_pipe_handler() {
#[cfg(target_family = "unix")]
{
use nix::sys::signal;
unsafe {
let _ = signal::signal(signal::Signal::SIGPIPE, signal::SigHandler::SigDfl)
.map_err(|e| eprintln!("{}", e));
}
}
}