From ad3f21b0e549b2f6726f46e8695bb76ceb3d066f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 11 Sep 2019 14:44:49 -0700 Subject: [PATCH] Add simple CLI command for running tree queries --- cli/src/lib.rs | 1 + cli/src/main.rs | 120 +++++++++++++++++++++++++++++++---------------- cli/src/query.rs | 49 +++++++++++++++++++ 3 files changed, 130 insertions(+), 40 deletions(-) create mode 100644 cli/src/query.rs diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 33a9904f..6a7c9507 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -5,6 +5,7 @@ pub mod highlight; pub mod loader; pub mod logger; pub mod parse; +pub mod query; pub mod test; pub mod util; pub mod wasm; diff --git a/cli/src/main.rs b/cli/src/main.rs index 59d04a97..9e8f885b 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,10 +1,12 @@ use clap::{App, AppSettings, Arg, SubCommand}; use error::Error; +use std::io::Read; use std::path::Path; use std::process::exit; -use std::{env, fs, u64}; +use std::{env, fs, io, u64}; +use tree_sitter::Language; use tree_sitter_cli::{ - config, error, generate, highlight, loader, logger, parse, test, wasm, web_ui, + config, error, generate, highlight, loader, logger, parse, query, test, wasm, web_ui, }; const BUILD_VERSION: &'static str = env!("CARGO_PKG_VERSION"); @@ -50,7 +52,7 @@ fn run() -> error::Result<()> { ) .subcommand( SubCommand::with_name("parse") - .about("Parse a file") + .about("Parse files") .arg( Arg::with_name("path") .index(1) @@ -73,6 +75,18 @@ fn run() -> error::Result<()> { .number_of_values(1), ), ) + .subcommand( + SubCommand::with_name("query") + .about("Search files using a syntax tree query") + .arg(Arg::with_name("query-path").index(1).required(true)) + .arg( + Arg::with_name("path") + .index(2) + .multiple(true) + .required(true), + ) + .arg(Arg::with_name("scope").long("scope").takes_value(true)), + ) .subcommand( SubCommand::with_name("test") .about("Run a parser's tests") @@ -168,7 +182,6 @@ fn run() -> error::Result<()> { let timeout = matches .value_of("timeout") .map_or(0, |t| u64::from_str_radix(t, 10).unwrap()); - loader.find_all_languages(&config.parser_directories)?; let paths = matches .values_of("path") .unwrap() @@ -176,43 +189,11 @@ fn run() -> error::Result<()> { .collect::>(); let max_path_length = paths.iter().map(|p| p.chars().count()).max().unwrap(); let mut has_error = false; + loader.find_all_languages(&config.parser_directories)?; for path in paths { let path = Path::new(path); - let language = if let Some(scope) = matches.value_of("scope") { - if let Some(config) = - loader - .language_configuration_for_scope(scope) - .map_err(Error::wrap(|| { - format!("Failed to load language for scope '{}'", scope) - }))? - { - config.0 - } else { - return Error::err(format!("Unknown scope '{}'", scope)); - } - } else if let Some((lang, _)) = loader - .language_configuration_for_file_name(path) - .map_err(Error::wrap(|| { - format!( - "Failed to load language for file name {:?}", - path.file_name().unwrap() - ) - }))? - { - lang - } else if let Some(lang) = loader - .languages_at_path(¤t_dir) - .map_err(Error::wrap(|| { - "Failed to load language in current directory" - }))? - .first() - .cloned() - { - lang - } else { - eprintln!("No language found"); - return Ok(()); - }; + let language = + select_language(&mut loader, path, ¤t_dir, matches.value_of("scope"))?; has_error |= parse::parse_file_at_path( language, path, @@ -226,10 +207,25 @@ fn run() -> error::Result<()> { allow_cancellation, )?; } - if has_error { return Error::err(String::new()); } + } else if let Some(matches) = matches.subcommand_matches("query") { + let paths = matches + .values_of("path") + .unwrap() + .into_iter() + .map(Path::new) + .collect::>(); + loader.find_all_languages(&config.parser_directories)?; + let language = select_language( + &mut loader, + paths[0], + ¤t_dir, + matches.value_of("scope"), + )?; + let query_path = Path::new(matches.value_of("query-path").unwrap()); + query::query_files_at_paths(language, paths, query_path)?; } else if let Some(matches) = matches.subcommand_matches("highlight") { let paths = matches.values_of("path").unwrap().into_iter(); let html_mode = matches.is_present("html"); @@ -296,3 +292,47 @@ fn run() -> error::Result<()> { Ok(()) } + +fn select_language( + loader: &mut loader::Loader, + path: &Path, + current_dir: &Path, + scope: Option<&str>, +) -> Result { + if let Some(scope) = scope { + if let Some(config) = + loader + .language_configuration_for_scope(scope) + .map_err(Error::wrap(|| { + format!("Failed to load language for scope '{}'", scope) + }))? + { + Ok(config.0) + } else { + return Error::err(format!("Unknown scope '{}'", scope)); + } + } else if let Some((lang, _)) = + loader + .language_configuration_for_file_name(path) + .map_err(Error::wrap(|| { + format!( + "Failed to load language for file name {:?}", + path.file_name().unwrap() + ) + }))? + { + Ok(lang) + } else if let Some(lang) = loader + .languages_at_path(¤t_dir) + .map_err(Error::wrap(|| { + "Failed to load language in current directory" + }))? + .first() + .cloned() + { + Ok(lang) + } else { + eprintln!("No language found"); + Error::err("No language found".to_string()) + } +} diff --git a/cli/src/query.rs b/cli/src/query.rs new file mode 100644 index 00000000..74f3e327 --- /dev/null +++ b/cli/src/query.rs @@ -0,0 +1,49 @@ +use super::error::{Error, Result}; +use std::fs; +use std::io::{self, Write}; +use std::path::Path; +use tree_sitter::{Language, Parser, Query}; + +pub fn query_files_at_paths( + language: Language, + paths: Vec<&Path>, + query_path: &Path, +) -> Result<()> { + let stdout = io::stdout(); + let mut stdout = stdout.lock(); + + let query_source = fs::read_to_string(query_path).map_err(Error::wrap(|| { + format!("Error reading query file {:?}", query_path) + }))?; + let query = Query::new(language, &query_source) + .map_err(|e| Error::new(format!("Query compilation failed: {:?}", e)))?; + + let query_context = query.context(); + + let mut parser = Parser::new(); + parser.set_language(language).map_err(|e| e.to_string())?; + + for path in paths { + writeln!(&mut stdout, "{}", path.to_str().unwrap())?; + + let source_code = fs::read(path).map_err(Error::wrap(|| { + format!("Error reading source file {:?}", path) + }))?; + + let tree = parser.parse(&source_code, None).unwrap(); + + for mat in query_context.exec(tree.root_node()) { + writeln!(&mut stdout, " pattern: {}", mat.pattern_index())?; + for (capture_id, node) in mat.captures() { + writeln!( + &mut stdout, + " {}: {:?}", + &query.capture_names()[capture_id], + node.utf8_text(&source_code).unwrap_or("") + )?; + } + } + } + + Ok(()) +}