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..8de7ed67 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -3,8 +3,9 @@ use error::Error; use std::path::Path; use std::process::exit; use std::{env, fs, 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 +51,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 +74,19 @@ 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)) + .arg(Arg::with_name("captures").long("captures").short("c")), + ) .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,26 @@ fn run() -> error::Result<()> { allow_cancellation, )?; } - if has_error { return Error::err(String::new()); } + } else if let Some(matches) = matches.subcommand_matches("query") { + let ordered_captures = matches.values_of("captures").is_some(); + 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, ordered_captures)?; } 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 +293,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..f373a314 --- /dev/null +++ b/cli/src/query.rs @@ -0,0 +1,64 @@ +use super::error::{Error, Result}; +use std::fs; +use std::io::{self, Write}; +use std::path::Path; +use tree_sitter::{Language, Node, Parser, Query, QueryCursor}; + +pub fn query_files_at_paths( + language: Language, + paths: Vec<&Path>, + query_path: &Path, + ordered_captures: bool, +) -> 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 mut query_cursor = QueryCursor::new(); + + 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 text_callback = |n: Node| &source_code[n.byte_range()]; + let tree = parser.parse(&source_code, None).unwrap(); + + if ordered_captures { + for (pattern_index, capture) in query_cursor.captures(&query, tree.root_node(), text_callback) { + writeln!( + &mut stdout, + " pattern: {}, capture: {}, row: {}, text: {:?}", + pattern_index, + &query.capture_names()[capture.index], + capture.node.start_position().row, + capture.node.utf8_text(&source_code).unwrap_or("") + )?; + } + } else { + for m in query_cursor.matches(&query, tree.root_node(), text_callback) { + writeln!(&mut stdout, " pattern: {}", m.pattern_index)?; + for capture in m.captures() { + writeln!( + &mut stdout, + " capture: {}, row: {}, text: {:?}", + &query.capture_names()[capture.index], + capture.node.start_position().row, + capture.node.utf8_text(&source_code).unwrap_or("") + )?; + } + } + } + } + + Ok(()) +} diff --git a/cli/src/tests/helpers/allocations.rs b/cli/src/tests/helpers/allocations.rs index c64762bd..2f89c173 100644 --- a/cli/src/tests/helpers/allocations.rs +++ b/cli/src/tests/helpers/allocations.rs @@ -51,6 +51,12 @@ pub fn stop_recording() { } } +pub fn record(f: impl FnOnce()) { + start_recording(); + f(); + stop_recording(); +} + fn record_alloc(ptr: *mut c_void) { let mut recorder = RECORDER.lock(); if recorder.enabled { diff --git a/cli/src/tests/mod.rs b/cli/src/tests/mod.rs index 143e8297..1a2a71ff 100644 --- a/cli/src/tests/mod.rs +++ b/cli/src/tests/mod.rs @@ -4,4 +4,5 @@ mod highlight_test; mod node_test; mod parser_test; mod properties_test; +mod query_test; mod tree_test; diff --git a/cli/src/tests/query_test.rs b/cli/src/tests/query_test.rs new file mode 100644 index 00000000..7373bb32 --- /dev/null +++ b/cli/src/tests/query_test.rs @@ -0,0 +1,897 @@ +use super::helpers::allocations; +use super::helpers::fixtures::get_language; +use std::fmt::Write; +use tree_sitter::{Node, Parser, Query, QueryCapture, QueryCursor, QueryError, QueryMatch}; + +#[test] +fn test_query_errors_on_invalid_syntax() { + allocations::record(|| { + let language = get_language("javascript"); + + assert!(Query::new(language, "(if_statement)").is_ok()); + assert!(Query::new(language, "(if_statement condition:(identifier))").is_ok()); + + // Mismatched parens + assert_eq!( + Query::new(language, "(if_statement"), + Err(QueryError::Syntax(13)) + ); + assert_eq!( + Query::new(language, "(if_statement))"), + Err(QueryError::Syntax(14)) + ); + + // Return an error at the *beginning* of a bare identifier not followed a colon. + // If there's a colon but no pattern, return an error at the end of the colon. + assert_eq!( + Query::new(language, "(if_statement identifier)"), + Err(QueryError::Syntax(14)) + ); + assert_eq!( + Query::new(language, "(if_statement condition:)"), + Err(QueryError::Syntax(24)) + ); + + // Return an error at the beginning of an unterminated string. + assert_eq!( + Query::new(language, r#"(identifier) "h "#), + Err(QueryError::Syntax(13)) + ); + + assert_eq!( + Query::new(language, r#"((identifier) ()"#), + Err(QueryError::Syntax(16)) + ); + assert_eq!( + Query::new(language, r#"((identifier) @x (eq? @x a"#), + Err(QueryError::Syntax(26)) + ); + }); +} + +#[test] +fn test_query_errors_on_invalid_symbols() { + allocations::record(|| { + let language = get_language("javascript"); + + assert_eq!( + Query::new(language, "(clas)"), + Err(QueryError::NodeType("clas")) + ); + assert_eq!( + Query::new(language, "(if_statement (arrayyyyy))"), + Err(QueryError::NodeType("arrayyyyy")) + ); + assert_eq!( + Query::new(language, "(if_statement condition: (non_existent3))"), + Err(QueryError::NodeType("non_existent3")) + ); + assert_eq!( + Query::new(language, "(if_statement condit: (identifier))"), + Err(QueryError::Field("condit")) + ); + assert_eq!( + Query::new(language, "(if_statement conditioning: (identifier))"), + Err(QueryError::Field("conditioning")) + ); + }); +} + +#[test] +fn test_query_errors_on_invalid_conditions() { + allocations::record(|| { + let language = get_language("javascript"); + + assert_eq!( + Query::new(language, "((identifier) @id (@id))"), + Err(QueryError::Predicate( + "Expected predicate to start with a function name. Got @id.".to_string() + )) + ); + assert_eq!( + Query::new(language, "((identifier) @id (eq? @id))"), + Err(QueryError::Predicate( + "Wrong number of arguments to eq? predicate. Expected 2, got 1.".to_string() + )) + ); + assert_eq!( + Query::new(language, "((identifier) @id (eq? @id @ok))"), + Err(QueryError::Capture("ok")) + ); + }); +} + +#[test] +fn test_query_matches_with_simple_pattern() { + allocations::record(|| { + let language = get_language("javascript"); + let query = Query::new( + language, + "(function_declaration name: (identifier) @fn-name)", + ) + .unwrap(); + + let source = "function one() { two(); function three() {} }"; + let mut parser = Parser::new(); + parser.set_language(language).unwrap(); + let tree = parser.parse(source, None).unwrap(); + + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(&query, tree.root_node(), to_callback(source)); + + assert_eq!( + collect_matches(matches, &query, source), + &[ + (0, vec![("fn-name", "one")]), + (0, vec![("fn-name", "three")]) + ], + ); + }); +} + +#[test] +fn test_query_matches_with_multiple_on_same_root() { + allocations::record(|| { + let language = get_language("javascript"); + let query = Query::new( + language, + "(class_declaration + name: (identifier) @the-class-name + (class_body + (method_definition + name: (property_identifier) @the-method-name)))", + ) + .unwrap(); + + let source = " + class Person { + // the constructor + constructor(name) { this.name = name; } + + // the getter + getFullName() { return this.name; } + } + "; + + let mut parser = Parser::new(); + parser.set_language(language).unwrap(); + let tree = parser.parse(source, None).unwrap(); + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(&query, tree.root_node(), to_callback(source)); + + assert_eq!( + collect_matches(matches, &query, source), + &[ + ( + 0, + vec![ + ("the-class-name", "Person"), + ("the-method-name", "constructor") + ] + ), + ( + 0, + vec![ + ("the-class-name", "Person"), + ("the-method-name", "getFullName") + ] + ), + ], + ); + }); +} + +#[test] +fn test_query_matches_with_multiple_patterns_different_roots() { + allocations::record(|| { + let language = get_language("javascript"); + let query = Query::new( + language, + " + (function_declaration name:(identifier) @fn-def) + (call_expression function:(identifier) @fn-ref) + ", + ) + .unwrap(); + + let source = " + function f1() { + f2(f3()); + } + "; + + let mut parser = Parser::new(); + parser.set_language(language).unwrap(); + let tree = parser.parse(source, None).unwrap(); + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(&query, tree.root_node(), to_callback(source)); + + assert_eq!( + collect_matches(matches, &query, source), + &[ + (0, vec![("fn-def", "f1")]), + (1, vec![("fn-ref", "f2")]), + (1, vec![("fn-ref", "f3")]), + ], + ); + }); +} + +#[test] +fn test_query_matches_with_multiple_patterns_same_root() { + allocations::record(|| { + let language = get_language("javascript"); + let query = Query::new( + language, + " + (pair + key: (property_identifier) @method-def + value: (function)) + + (pair + key: (property_identifier) @method-def + value: (arrow_function)) + ", + ) + .unwrap(); + + let source = " + a = { + b: () => { return c; }, + d: function() { return d; } + }; + "; + + let mut parser = Parser::new(); + parser.set_language(language).unwrap(); + let tree = parser.parse(source, None).unwrap(); + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(&query, tree.root_node(), to_callback(source)); + + assert_eq!( + collect_matches(matches, &query, source), + &[ + (1, vec![("method-def", "b")]), + (0, vec![("method-def", "d")]), + ], + ); + }); +} + +#[test] +fn test_query_matches_with_nesting_and_no_fields() { + allocations::record(|| { + let language = get_language("javascript"); + let query = Query::new( + language, + " + (array + (array + (identifier) @x1 + (identifier) @x2)) + ", + ) + .unwrap(); + + let source = " + [[a]]; + [[c, d], [e, f, g, h]]; + [[h], [i]]; + "; + + let mut parser = Parser::new(); + parser.set_language(language).unwrap(); + let tree = parser.parse(source, None).unwrap(); + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(&query, tree.root_node(), to_callback(source)); + + assert_eq!( + collect_matches(matches, &query, source), + &[ + (0, vec![("x1", "c"), ("x2", "d")]), + (0, vec![("x1", "e"), ("x2", "f")]), + (0, vec![("x1", "e"), ("x2", "g")]), + (0, vec![("x1", "f"), ("x2", "g")]), + (0, vec![("x1", "e"), ("x2", "h")]), + (0, vec![("x1", "f"), ("x2", "h")]), + (0, vec![("x1", "g"), ("x2", "h")]), + ], + ); + }); +} + +#[test] +fn test_query_matches_with_many() { + allocations::record(|| { + let language = get_language("javascript"); + let query = Query::new(language, "(array (identifier) @element)").unwrap(); + + let source = "[hello];\n".repeat(50); + + let mut parser = Parser::new(); + parser.set_language(language).unwrap(); + let tree = parser.parse(&source, None).unwrap(); + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(&query, tree.root_node(), to_callback(&source)); + + assert_eq!( + collect_matches(matches, &query, source.as_str()), + vec![(0, vec![("element", "hello")]); 50], + ); + }); +} + +#[test] +fn test_query_matches_with_too_many_permutations_to_track() { + allocations::record(|| { + let language = get_language("javascript"); + let query = Query::new( + language, + " + (array (identifier) @pre (identifier) @post) + ", + ) + .unwrap(); + + let mut source = "hello, ".repeat(50); + source.insert(0, '['); + source.push_str("];"); + + let mut parser = Parser::new(); + parser.set_language(language).unwrap(); + let tree = parser.parse(&source, None).unwrap(); + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(&query, tree.root_node(), to_callback(&source)); + + // For this pathological query, some match permutations will be dropped. + // Just check that a subset of the results are returned, and crash or + // leak occurs. + assert_eq!( + collect_matches(matches, &query, source.as_str())[0], + (0, vec![("pre", "hello"), ("post", "hello")]), + ); + }); +} + +#[test] +fn test_query_matches_with_anonymous_tokens() { + allocations::record(|| { + let language = get_language("javascript"); + let query = Query::new( + language, + r#" + ";" @punctuation + "&&" @operator + "#, + ) + .unwrap(); + + let source = "foo(a && b);"; + + let mut parser = Parser::new(); + parser.set_language(language).unwrap(); + let tree = parser.parse(&source, None).unwrap(); + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(&query, tree.root_node(), to_callback(source)); + + assert_eq!( + collect_matches(matches, &query, source), + &[ + (1, vec![("operator", "&&")]), + (0, vec![("punctuation", ";")]), + ] + ); + }); +} + +#[test] +fn test_query_matches_within_byte_range() { + allocations::record(|| { + let language = get_language("javascript"); + let query = Query::new(language, "(identifier) @element").unwrap(); + + let source = "[a, b, c, d, e, f, g]"; + + let mut parser = Parser::new(); + parser.set_language(language).unwrap(); + let tree = parser.parse(&source, None).unwrap(); + + let mut cursor = QueryCursor::new(); + let matches = + cursor + .set_byte_range(5, 15) + .matches(&query, tree.root_node(), to_callback(source)); + + assert_eq!( + collect_matches(matches, &query, source), + &[ + (0, vec![("element", "c")]), + (0, vec![("element", "d")]), + (0, vec![("element", "e")]), + ] + ); + }); +} + +#[test] +fn test_query_matches_different_queries_same_cursor() { + allocations::record(|| { + let language = get_language("javascript"); + let query1 = Query::new( + language, + " + (array (identifier) @id1) + ", + ) + .unwrap(); + let query2 = Query::new( + language, + " + (array (identifier) @id1) + (pair (identifier) @id2) + ", + ) + .unwrap(); + let query3 = Query::new( + language, + " + (array (identifier) @id1) + (pair (identifier) @id2) + (parenthesized_expression (identifier) @id3) + ", + ) + .unwrap(); + + let source = "[a, {b: b}, (c)];"; + + let mut parser = Parser::new(); + let mut cursor = QueryCursor::new(); + + parser.set_language(language).unwrap(); + let tree = parser.parse(&source, None).unwrap(); + + let matches = cursor.matches(&query1, tree.root_node(), to_callback(source)); + assert_eq!( + collect_matches(matches, &query1, source), + &[(0, vec![("id1", "a")]),] + ); + + let matches = cursor.matches(&query3, tree.root_node(), to_callback(source)); + assert_eq!( + collect_matches(matches, &query3, source), + &[ + (0, vec![("id1", "a")]), + (1, vec![("id2", "b")]), + (2, vec![("id3", "c")]), + ] + ); + + let matches = cursor.matches(&query2, tree.root_node(), to_callback(source)); + assert_eq!( + collect_matches(matches, &query2, source), + &[(0, vec![("id1", "a")]), (1, vec![("id2", "b")]),] + ); + }); +} + +#[test] +fn test_query_captures() { + allocations::record(|| { + let language = get_language("javascript"); + let query = Query::new( + language, + r#" + (pair + key: * @method.def + (function + name: (identifier) @method.alias)) + + (variable_declarator + name: * @function.def + value: (function + name: (identifier) @function.alias)) + + ":" @delimiter + "=" @operator + "#, + ) + .unwrap(); + + let source = " + a({ + bc: function de() { + const fg = function hi() {} + }, + jk: function lm() { + const no = function pq() {} + }, + }); + "; + + let mut parser = Parser::new(); + parser.set_language(language).unwrap(); + let tree = parser.parse(&source, None).unwrap(); + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(&query, tree.root_node(), to_callback(source)); + + assert_eq!( + collect_matches(matches, &query, source), + &[ + (2, vec![("delimiter", ":")]), + (0, vec![("method.def", "bc"), ("method.alias", "de")]), + (3, vec![("operator", "=")]), + (1, vec![("function.def", "fg"), ("function.alias", "hi")]), + (2, vec![("delimiter", ":")]), + (0, vec![("method.def", "jk"), ("method.alias", "lm")]), + (3, vec![("operator", "=")]), + (1, vec![("function.def", "no"), ("function.alias", "pq")]), + ], + ); + + let captures = cursor.captures(&query, tree.root_node(), to_callback(source)); + assert_eq!( + collect_captures(captures, &query, source), + &[ + ("method.def", "bc"), + ("delimiter", ":"), + ("method.alias", "de"), + ("function.def", "fg"), + ("operator", "="), + ("function.alias", "hi"), + ("method.def", "jk"), + ("delimiter", ":"), + ("method.alias", "lm"), + ("function.def", "no"), + ("operator", "="), + ("function.alias", "pq"), + ] + ); + }); +} + +#[test] +fn test_query_captures_with_text_conditions() { + allocations::record(|| { + let language = get_language("javascript"); + let query = Query::new( + language, + r#" + ((identifier) @constant + (match? @constant "^[A-Z]{2,}$")) + + ((identifier) @constructor + (match? @constructor "^[A-Z]")) + + ((identifier) @function.builtin + (eq? @function.builtin "require")) + + (identifier) @variable + "#, + ) + .unwrap(); + + let source = " + const ab = require('./ab'); + new Cd(EF); + "; + + let mut parser = Parser::new(); + parser.set_language(language).unwrap(); + let tree = parser.parse(&source, None).unwrap(); + let mut cursor = QueryCursor::new(); + + let captures = cursor.captures(&query, tree.root_node(), to_callback(source)); + assert_eq!( + collect_captures(captures, &query, source), + &[ + ("variable", "ab"), + ("function.builtin", "require"), + ("variable", "require"), + ("constructor", "Cd"), + ("variable", "Cd"), + ("constant", "EF"), + ("constructor", "EF"), + ("variable", "EF"), + ], + ); + }); +} + +#[test] +fn test_query_captures_with_duplicates() { + allocations::record(|| { + let language = get_language("javascript"); + let query = Query::new( + language, + r#" + (variable_declarator + name: (identifier) @function + value: (function)) + + (identifier) @variable + "#, + ) + .unwrap(); + + let source = " + var x = function() {}; + "; + + let mut parser = Parser::new(); + parser.set_language(language).unwrap(); + let tree = parser.parse(&source, None).unwrap(); + let mut cursor = QueryCursor::new(); + + let captures = cursor.captures(&query, tree.root_node(), to_callback(source)); + assert_eq!( + collect_captures(captures, &query, source), + &[("function", "x"), ("variable", "x"),], + ); + }); +} + +#[test] +fn test_query_captures_with_many_nested_results_without_fields() { + allocations::record(|| { + let language = get_language("javascript"); + + // Search for key-value pairs whose values are anonymous functions. + let query = Query::new( + language, + r#" + (pair + key: * @method-def + (arrow_function)) + + ":" @colon + "," @comma + "#, + ) + .unwrap(); + + // The `pair` node for key `y` does not match any pattern, but inside of + // its value, it contains many other `pair` nodes that do match the pattern. + // The match for the *outer* pair should be terminated *before* descending into + // the object value, so that we can avoid needing to buffer all of the inner + // matches. + let method_count = 50; + let mut source = "x = { y: {\n".to_owned(); + for i in 0..method_count { + writeln!(&mut source, " method{}: $ => null,", i).unwrap(); + } + source.push_str("}};\n"); + + let mut parser = Parser::new(); + parser.set_language(language).unwrap(); + let tree = parser.parse(&source, None).unwrap(); + let mut cursor = QueryCursor::new(); + + let captures = cursor.captures(&query, tree.root_node(), to_callback(&source)); + let captures = collect_captures(captures, &query, &source); + + assert_eq!( + &captures[0..13], + &[ + ("colon", ":"), + ("method-def", "method0"), + ("colon", ":"), + ("comma", ","), + ("method-def", "method1"), + ("colon", ":"), + ("comma", ","), + ("method-def", "method2"), + ("colon", ":"), + ("comma", ","), + ("method-def", "method3"), + ("colon", ":"), + ("comma", ","), + ] + ); + + // Ensure that we don't drop matches because of needing to buffer too many. + assert_eq!(captures.len(), 1 + 3 * method_count); + }); +} + +#[test] +fn test_query_captures_with_many_nested_results_with_fields() { + allocations::record(|| { + let language = get_language("javascript"); + + // Search expressions like `a ? a.b : null` + let query = Query::new( + language, + r#" + ((ternary_expression + condition: (identifier) @left + consequence: (member_expression + object: (identifier) @right) + alternative: (null)) + (eq? @left @right)) + "#, + ) + .unwrap(); + + // The outer expression does not match the pattern, but the consequence of the ternary + // is an object that *does* contain many occurences of the pattern. + let count = 50; + let mut source = "a ? {".to_owned(); + for i in 0..count { + writeln!(&mut source, " x: y{} ? y{}.z : null,", i, i).unwrap(); + } + source.push_str("} : null;\n"); + + let mut parser = Parser::new(); + parser.set_language(language).unwrap(); + let tree = parser.parse(&source, None).unwrap(); + let mut cursor = QueryCursor::new(); + + let captures = cursor.captures(&query, tree.root_node(), to_callback(&source)); + let captures = collect_captures(captures, &query, &source); + + assert_eq!( + &captures[0..20], + &[ + ("left", "y0"), + ("right", "y0"), + ("left", "y1"), + ("right", "y1"), + ("left", "y2"), + ("right", "y2"), + ("left", "y3"), + ("right", "y3"), + ("left", "y4"), + ("right", "y4"), + ("left", "y5"), + ("right", "y5"), + ("left", "y6"), + ("right", "y6"), + ("left", "y7"), + ("right", "y7"), + ("left", "y8"), + ("right", "y8"), + ("left", "y9"), + ("right", "y9"), + ] + ); + + // Ensure that we don't drop matches because of needing to buffer too many. + assert_eq!(captures.len(), 2 * count); + }); +} + +#[test] +fn test_query_start_byte_for_pattern() { + let language = get_language("javascript"); + + let patterns_1 = r#" + "+" @operator + "-" @operator + "*" @operator + "=" @operator + "=>" @operator + "# + .trim_start(); + + let patterns_2 = " + (identifier) @a + (string) @b + " + .trim_start(); + + let patterns_3 = " + ((identifier) @b (match? @b i)) + (function_declaration name: (identifier) @c) + (method_definition name: (identifier) @d) + " + .trim_start(); + + let mut source = String::new(); + source += patterns_1; + source += patterns_2; + source += patterns_3; + + let query = Query::new(language, &source).unwrap(); + + assert_eq!(query.start_byte_for_pattern(0), 0); + assert_eq!(query.start_byte_for_pattern(5), patterns_1.len()); + assert_eq!( + query.start_byte_for_pattern(7), + patterns_1.len() + patterns_2.len() + ); +} + +#[test] +fn test_query_capture_names() { + allocations::record(|| { + let language = get_language("javascript"); + let query = Query::new( + language, + r#" + (if_statement + condition: (binary_expression + left: * @left-operand + operator: "||" + right: * @right-operand) + consequence: (statement_block) @body) + + (while_statement + condition:* @loop-condition) + "#, + ) + .unwrap(); + + assert_eq!( + query.capture_names(), + &[ + "left-operand".to_string(), + "right-operand".to_string(), + "body".to_string(), + "loop-condition".to_string(), + ] + ); + }); +} + +#[test] +fn test_query_comments() { + allocations::record(|| { + let language = get_language("javascript"); + let query = Query::new( + language, + " + ; this is my first comment + ; i have two comments here + (function_declaration + ; there is also a comment here + ; and here + name: (identifier) @fn-name)", + ) + .unwrap(); + + let source = "function one() { }"; + let mut parser = Parser::new(); + parser.set_language(language).unwrap(); + let tree = parser.parse(source, None).unwrap(); + let mut cursor = QueryCursor::new(); + let matches = cursor.matches(&query, tree.root_node(), to_callback(source)); + assert_eq!( + collect_matches(matches, &query, source), + &[(0, vec![("fn-name", "one")]),], + ); + }); +} + +fn collect_matches<'a>( + matches: impl Iterator>, + query: &'a Query, + source: &'a str, +) -> Vec<(usize, Vec<(&'a str, &'a str)>)> { + matches + .map(|m| { + ( + m.pattern_index, + collect_captures(m.captures().map(|c| (m.pattern_index, c)), query, source), + ) + }) + .collect() +} + +fn collect_captures<'a, 'b>( + captures: impl Iterator)>, + query: &'b Query, + source: &'b str, +) -> Vec<(&'b str, &'b str)> { + captures + .map(|(_, QueryCapture { index, node })| { + ( + query.capture_names()[index].as_str(), + node.utf8_text(source.as_bytes()).unwrap(), + ) + }) + .collect() +} + +fn to_callback<'a>(source: &'a str) -> impl Fn(Node) -> &'a [u8] { + move |n| &source.as_bytes()[n.byte_range()] +} diff --git a/cli/src/web_ui.html b/cli/src/web_ui.html index 2422a3d8..093b09ba 100644 --- a/cli/src/web_ui.html +++ b/cli/src/web_ui.html @@ -7,7 +7,7 @@ -
+