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::*; /// Generates list of strategies and functions generating sample data, all used by the crate's binary. 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::(&g.function()).unwrap(); quote! { #id => { let s = #fun; println!("{}", s.new_tree(&mut runner).unwrap().current()); } } }) .collect::>(); #[rustfmt::skip] let generated_module = quote! { mod strategy_list { use proptest::strategy::ValueTree; use proptest::{strategy::Strategy, test_runner::TestRunner}; // List of identifiers of usable strategies. pub const STRATEGIES: &[&str] = &[#(#strategy_ids),*]; // Prints sample of given strategy 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, pub fn_name: String, pub comment: Option, } 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::>(); 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, parser: &mut Parser, ) -> Vec { 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, name: &str, _comment: String, module_node: &Node, parser: &mut Parser, ) -> Vec { 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, code: &[u8], node: &Node, parser: &mut Parser, ) -> Vec { 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" => { // At the moment only care about string strategies without parameters. let is_string_strategy = child .child_by_field_name("return_type") .and_then(|n| n.utf8_text(code).ok()) == Some("impl Strategy"); 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() } /// Collects comment lines above given node. 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() } }