use std::env;
use std::path::Path;
fn main() {
let out_dir = env::var_os("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("strategy_list.rs");
gen::strategy_list(&dest_path);
}
mod gen {
use std::path::Path;
use quote::quote;
use tree_sitter::*;
pub fn strategy_list(path: &Path) {
let mut parser = Parser::new();
parser.set_language(&tree_sitter_rust::language()).unwrap();
let generators = walk_file(
Path::new("src"),
"lib.rs",
vec![String::from("bitcoin_proptest")],
&mut parser,
);
let strategy_ids = generators.iter().map(|g| g.id());
#[rustfmt::skip]
let samples = generators
.iter()
.map(|g| {
let id = g.id();
let fun = syn::parse_str::<syn::Expr>(&g.function()).unwrap();
quote! {
#id => {
let s = #fun;
println!("{}", s.new_tree(&mut runner).unwrap().current());
}
}
})
.collect::<Vec<_>>();
#[rustfmt::skip]
let generated_module = quote! {
mod strategy_list {
use proptest::strategy::ValueTree;
use proptest::{strategy::Strategy, test_runner::TestRunner};
pub const STRATEGIES: &[&str] = &[#(#strategy_ids),*];
pub fn sample(strategy_id: &str) {
let mut runner = TestRunner::new(Default::default());
match strategy_id {
#(#samples)*
_ => panic!("Unknown strategy, use -l to see list")
}
}
}
};
std::fs::write(path, generated_module.to_string()).unwrap();
}
#[derive(Debug)]
#[allow(dead_code)]
struct Generator {
pub path: Vec<String>,
pub fn_name: String,
pub comment: Option<String>,
}
impl Generator {
pub fn function(&self) -> String {
let path = self.path.join("::");
format!("{path}::{}()", self.fn_name)
}
pub fn id(&self) -> String {
let mut id = self.path.iter().skip(2).cloned().collect::<Vec<_>>();
if &self.fn_name != "hex" && &self.fn_name != "string" {
id.push(self.fn_name.clone());
}
id.join("/")
}
}
fn walk_file(
dir: &Path,
file: &str,
module: Vec<String>,
parser: &mut Parser,
) -> Vec<Generator> {
let code = std::fs::read_to_string(dir.join(file)).unwrap();
let tree = parser.parse(code.clone(), None).unwrap();
walk_node(dir, module, code.as_bytes(), &tree.root_node(), parser)
}
fn walk_mod(
dir: &Path,
code: &[u8],
module: Vec<String>,
name: &str,
_comment: String,
module_node: &Node,
parser: &mut Parser,
) -> Vec<Generator> {
if let Some(decl) = module_node
.named_children(&mut module_node.walk())
.find(|c| c.grammar_name() == "declaration_list")
{
walk_node(&dir.join(name), module, code, &decl, parser)
} else {
let mod_file_name = format!("{name}.rs");
if dir.join(&mod_file_name).exists() {
walk_file(dir, &mod_file_name, module, parser)
} else {
let mod_dir_path = dir.join(name);
if mod_dir_path.join("mod.rs").exists() {
walk_file(&mod_dir_path, "mod.rs", module, parser)
} else {
vec![]
}
}
}
}
fn walk_node(
dir: &Path,
module: Vec<String>,
code: &[u8],
node: &Node,
parser: &mut Parser,
) -> Vec<Generator> {
node.named_children(&mut node.walk())
.flat_map(|child| match child.grammar_name() {
"mod_item" => {
let mod_name = child
.child_by_field_name("name")
.expect("module name")
.utf8_text(code)
.expect("valid name");
let comment = collect_comments(&child, code).join("\n");
if mod_name != "test" && mod_name != "tests" {
let mut module = module.clone();
module.push(mod_name.to_string());
walk_mod(dir, code, module, mod_name, comment, &child, parser)
} else {
vec![]
}
}
"function_item" => {
let is_string_strategy = child
.child_by_field_name("return_type")
.and_then(|n| n.utf8_text(code).ok())
== Some("impl Strategy<Value = String>");
let is_empty = child
.child_by_field_name("parameters")
.map(|n| n.named_child_count())
== Some(0);
if is_string_strategy && is_empty {
let comment = Some(collect_comments(&child, code).join("\n"))
.filter(|c| !c.is_empty());
let function_name = child.child_by_field_name("name").expect("xxx");
vec![Generator {
path: module.clone(),
fn_name: function_name.utf8_text(code).expect("yyy").to_string(),
comment,
}]
} else {
vec![]
}
}
_ => vec![],
})
.collect()
}
fn collect_comments<'a>(node: &Node, code: &'a [u8]) -> Vec<&'a str> {
node.prev_named_sibling()
.filter(|prev| prev.grammar_name() == "line_comment")
.map(|comment_node| {
let mut lines_above = collect_comments(&comment_node, code);
let current_line = comment_node
.utf8_text(code)
.ok()
.map(|c| c.trim_start_matches("///").trim());
if let Some(line) = current_line {
lines_above.push(line);
}
lines_above
})
.unwrap_or_default()
}
}