feat(cli): rework query to use new input handler

Co-authored-by: Will Lillis <will.lillis24@gmail.com>
This commit is contained in:
Amaan Qureshi 2025-01-03 04:13:13 -05:00 committed by Will Lillis
parent 6bad1bc6c5
commit 88d2f010f5
3 changed files with 189 additions and 108 deletions

View file

@ -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<String>,
pub paths_file: Option<PathBuf>,
#[arg(index = 2, num_args=1.., help = "The source file(s) to use")]
pub paths: Option<Vec<String>>,
pub paths: Option<Vec<PathBuf>>,
#[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<PathBuf>,
#[arg(long, short = 'n', help = "Query the contents of a specific test")]
#[clap(conflicts_with = "paths", conflicts_with = "paths_file")]
pub test_number: Option<u32>,
}
#[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::<Vec<String>>();
if !paths.is_empty() {
.collect::<Vec<_>>();
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(())
}
}

View file

@ -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<String>,
path: &Path,
name: &str,
query_path: &Path,
ordered_captures: bool,
byte_range: Option<Range<usize>>,
@ -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(())

View file

@ -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<usize> {