diff --git a/cli/src/main.rs b/cli/src/main.rs index b3225ed8..f41bf72e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -327,7 +327,7 @@ struct Fuzz { #[command(about = "Search files using a syntax tree query", alias = "q")] struct Query { #[arg(help = "Path to a file with queries", index = 1, required = true)] - query_path: String, + query_path: PathBuf, #[arg(long, short, help = "Measure execution time")] pub time: bool, #[arg(long, short, help = "Suppress main output")] @@ -336,9 +336,9 @@ struct Query { long = "paths", help = "The path to a file with paths to source file(s)" )] - pub paths_file: Option, + pub paths_file: Option, #[arg(index = 2, num_args=1.., help = "The source file(s) to use")] - pub paths: Option>, + pub paths: Option>, #[arg( long, help = "The range of byte offsets in which the query will be executed" @@ -357,6 +357,9 @@ struct Query { pub test: bool, #[arg(long, help = "The path to an alternative config.json file")] pub config_path: Option, + #[arg(long, short = 'n', help = "Query the contents of a specific test")] + #[clap(conflicts_with = "paths", conflicts_with = "paths_file")] + pub test_number: Option, } #[derive(Args)] @@ -1084,31 +1087,37 @@ impl Test { .map(|s| s.to_str().unwrap_or_default()) .unwrap_or_default(); if stem != "highlights" && stem != "tags" { - let paths = walkdir::WalkDir::new(test_dir.join(stem)) + let entries = walkdir::WalkDir::new(test_dir.join(stem)) .into_iter() .filter_map(|e| { let entry = e.ok()?; if entry.file_type().is_file() { - Some(String::from(entry.path().to_string_lossy())) + Some(entry) } else { None } }) - .collect::>(); - if !paths.is_empty() { + .collect::>(); + if !entries.is_empty() { println!("{stem}:"); } - query::query_files_at_paths( - language, - paths, - entry.path(), - false, - None, - None, - true, - false, - false, - )?; + + for entry in entries { + let path = entry.path(); + query::query_file_at_path( + language, + path, + &path.display().to_string(), + path, + false, + None, + None, + true, + false, + false, + false, + )?; + } } } Ok(()) @@ -1156,11 +1165,8 @@ impl Fuzz { impl Query { fn run(self, mut loader: loader::Loader, current_dir: &Path) -> Result<()> { let config = Config::load(self.config_path)?; - let paths = collect_paths(self.paths_file.as_deref(), self.paths)?; let loader_config = config.get()?; loader.find_all_languages(&loader_config)?; - let language = - loader.select_language(Path::new(&paths[0]), current_dir, self.scope.as_deref())?; let query_path = Path::new(&self.query_path); let byte_range = self.byte_range.as_ref().and_then(|range| { @@ -1176,17 +1182,90 @@ impl Query { Some(Point::new(start, 0)..Point::new(end, 0)) }); - query::query_files_at_paths( - &language, - paths, - query_path, - self.captures, - byte_range, - point_range, - self.test, - self.quiet, - self.time, + let cancellation_flag = util::cancel_on_signal(); + + let input = get_input( + self.paths_file.as_deref(), + self.paths, + self.test_number, + &cancellation_flag, )?; + + match input { + CliInput::Paths(paths) => { + let language = loader.select_language( + Path::new(&paths[0]), + current_dir, + self.scope.as_deref(), + )?; + + for path in paths { + query::query_file_at_path( + &language, + &path, + &path.display().to_string(), + query_path, + self.captures, + byte_range.clone(), + point_range.clone(), + self.test, + self.quiet, + self.time, + false, + )?; + } + } + CliInput::Test { + name, + contents, + languages: language_names, + } => { + let path = get_tmp_source_file(&contents)?; + let languages = loader.languages_at_path(current_dir)?; + let language = languages + .iter() + .find(|(_, n)| language_names.contains(&Box::from(n.as_str()))) + .or_else(|| languages.first()) + .map(|(l, _)| l.clone()) + .ok_or_else(|| anyhow!("No language found"))?; + query::query_file_at_path( + &language, + &path, + &name, + query_path, + self.captures, + byte_range, + point_range, + self.test, + self.quiet, + self.time, + true, + )?; + fs::remove_file(path)?; + } + CliInput::Stdin(contents) => { + // Place user input and query output on separate lines + println!(); + + let path = get_tmp_source_file(&contents)?; + let language = loader.select_language(&path, current_dir, None)?; + query::query_file_at_path( + &language, + &path, + "stdin", + query_path, + self.captures, + byte_range, + point_range, + self.test, + self.quiet, + self.time, + true, + )?; + fs::remove_file(path)?; + } + } + Ok(()) } } diff --git a/cli/src/query.rs b/cli/src/query.rs index 2d8a1013..ea961880 100644 --- a/cli/src/query.rs +++ b/cli/src/query.rs @@ -17,9 +17,10 @@ use crate::{ }; #[allow(clippy::too_many_arguments)] -pub fn query_files_at_paths( +pub fn query_file_at_path( language: &Language, - paths: Vec, + path: &Path, + name: &str, query_path: &Path, ordered_captures: bool, byte_range: Option>, @@ -27,6 +28,7 @@ pub fn query_files_at_paths( should_test: bool, quiet: bool, print_time: bool, + stdin: bool, ) -> Result<()> { let stdout = io::stdout(); let mut stdout = stdout.lock(); @@ -46,26 +48,24 @@ pub fn query_files_at_paths( let mut parser = Parser::new(); parser.set_language(language)?; - for path in paths { - let mut results = Vec::new(); + let mut results = Vec::new(); - if !should_test { - writeln!(&mut stdout, "{path}")?; - } + if !should_test && !stdin { + writeln!(&mut stdout, "{name}")?; + } - let source_code = - fs::read(&path).with_context(|| format!("Error reading source file {path:?}"))?; - let tree = parser.parse(&source_code, None).unwrap(); + let source_code = + fs::read(path).with_context(|| format!("Error reading source file {path:?}"))?; + let tree = parser.parse(&source_code, None).unwrap(); - let start = Instant::now(); - if ordered_captures { - let mut captures = - query_cursor.captures(&query, tree.root_node(), source_code.as_slice()); - while let Some((mat, capture_index)) = captures.next() { - let capture = mat.captures[*capture_index]; - let capture_name = &query.capture_names()[capture.index as usize]; - if !quiet && !should_test { - writeln!( + let start = Instant::now(); + if ordered_captures { + let mut captures = query_cursor.captures(&query, tree.root_node(), source_code.as_slice()); + while let Some((mat, capture_index)) = captures.next() { + let capture = mat.captures[*capture_index]; + let capture_name = &query.capture_names()[capture.index as usize]; + if !quiet && !should_test { + writeln!( &mut stdout, " pattern: {:>2}, capture: {} - {capture_name}, start: {}, end: {}, text: `{}`", mat.pattern_index, @@ -74,6 +74,37 @@ pub fn query_files_at_paths( capture.node.end_position(), capture.node.utf8_text(&source_code).unwrap_or("") )?; + } + results.push(query_testing::CaptureInfo { + name: (*capture_name).to_string(), + start: to_utf8_point(capture.node.start_position(), source_code.as_slice()), + end: to_utf8_point(capture.node.end_position(), source_code.as_slice()), + }); + } + } else { + let mut matches = query_cursor.matches(&query, tree.root_node(), source_code.as_slice()); + while let Some(m) = matches.next() { + if !quiet && !should_test { + writeln!(&mut stdout, " pattern: {}", m.pattern_index)?; + } + for capture in m.captures { + let start = capture.node.start_position(); + let end = capture.node.end_position(); + let capture_name = &query.capture_names()[capture.index as usize]; + if !quiet && !should_test { + if end.row == start.row { + writeln!( + &mut stdout, + " capture: {} - {capture_name}, start: {start}, end: {end}, text: `{}`", + capture.index, + capture.node.utf8_text(&source_code).unwrap_or("") + )?; + } else { + writeln!( + &mut stdout, + " capture: {capture_name}, start: {start}, end: {end}", + )?; + } } results.push(query_testing::CaptureInfo { name: (*capture_name).to_string(), @@ -81,65 +112,36 @@ pub fn query_files_at_paths( end: to_utf8_point(capture.node.end_position(), source_code.as_slice()), }); } + } + } + if query_cursor.did_exceed_match_limit() { + writeln!( + &mut stdout, + " WARNING: Query exceeded maximum number of in-progress captures!" + )?; + } + if should_test { + let path_name = if stdin { + "stdin" } else { - let mut matches = - query_cursor.matches(&query, tree.root_node(), source_code.as_slice()); - while let Some(m) = matches.next() { - if !quiet && !should_test { - writeln!(&mut stdout, " pattern: {}", m.pattern_index)?; - } - for capture in m.captures { - let start = capture.node.start_position(); - let end = capture.node.end_position(); - let capture_name = &query.capture_names()[capture.index as usize]; - if !quiet && !should_test { - if end.row == start.row { - writeln!( - &mut stdout, - " capture: {} - {capture_name}, start: {start}, end: {end}, text: `{}`", - capture.index, - capture.node.utf8_text(&source_code).unwrap_or("") - )?; - } else { - writeln!( - &mut stdout, - " capture: {capture_name}, start: {start}, end: {end}", - )?; - } - } - results.push(query_testing::CaptureInfo { - name: (*capture_name).to_string(), - start: to_utf8_point(capture.node.start_position(), source_code.as_slice()), - end: to_utf8_point(capture.node.end_position(), source_code.as_slice()), - }); - } + Path::new(&path).file_name().unwrap().to_str().unwrap() + }; + match query_testing::assert_expected_captures(&results, path, &mut parser, language) { + Ok(assertion_count) => { + println!( + " ✓ {} ({} assertions)", + paint(Some(AnsiColor::Green), path_name), + assertion_count + ); + } + Err(e) => { + println!(" ✗ {}", paint(Some(AnsiColor::Red), path_name)); + return Err(e); } } - if query_cursor.did_exceed_match_limit() { - writeln!( - &mut stdout, - " WARNING: Query exceeded maximum number of in-progress captures!" - )?; - } - if should_test { - let path_name = Path::new(&path).file_name().unwrap().to_str().unwrap(); - match query_testing::assert_expected_captures(&results, &path, &mut parser, language) { - Ok(assertion_count) => { - println!( - " ✓ {} ({} assertions)", - paint(Some(AnsiColor::Green), path_name), - assertion_count - ); - } - Err(e) => { - println!(" ✗ {}", paint(Some(AnsiColor::Red), path_name)); - return Err(e); - } - } - } - if print_time { - writeln!(&mut stdout, "{:?}", start.elapsed())?; - } + } + if print_time { + writeln!(&mut stdout, "{:?}", start.elapsed())?; } Ok(()) diff --git a/cli/src/query_testing.rs b/cli/src/query_testing.rs index 17f9d2c7..3774abdf 100644 --- a/cli/src/query_testing.rs +++ b/cli/src/query_testing.rs @@ -1,4 +1,4 @@ -use std::fs; +use std::{fs, path::Path}; use anyhow::{anyhow, Result}; use bstr::{BStr, ByteSlice}; @@ -224,7 +224,7 @@ pub fn parse_position_comments( pub fn assert_expected_captures( infos: &[CaptureInfo], - path: &str, + path: &Path, parser: &mut Parser, language: &Language, ) -> Result {