diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 97c288a1..e00323b7 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -6,6 +6,7 @@ pub mod loader; pub mod logger; pub mod parse; pub mod query; +pub mod query_testing; pub mod tags; pub mod test; pub mod test_highlight; diff --git a/cli/src/query.rs b/cli/src/query.rs index de320fba..9c524877 100644 --- a/cli/src/query.rs +++ b/cli/src/query.rs @@ -1,13 +1,10 @@ use super::error::{Error, Result}; +use crate::query_testing; use std::fs; use std::io::{self, Write}; use std::path::Path; use tree_sitter::{Language, Node, Parser, Query, QueryCursor}; -mod assert; - -use assert::CaptureInfo; - pub fn query_files_at_paths( language: Language, paths: Vec, @@ -58,7 +55,7 @@ pub fn query_files_at_paths( capture.node.start_position().row, capture.node.utf8_text(&source_code).unwrap_or("") )?; - results.push(CaptureInfo { + results.push(query_testing::CaptureInfo { name: capture_name.to_string(), position: capture.node.start_position(), }); @@ -85,7 +82,7 @@ pub fn query_files_at_paths( capture_name, start, end, )?; } - results.push(CaptureInfo { + results.push(query_testing::CaptureInfo { name: capture_name.to_string(), position: capture.node.start_position(), }); @@ -93,7 +90,7 @@ pub fn query_files_at_paths( } } if should_test { - assert::assert_expected_captures(results, path, &mut parser, language)? + query_testing::assert_expected_captures(results, path, &mut parser, language)? } } diff --git a/cli/src/query/assert.rs b/cli/src/query/assert.rs deleted file mode 100644 index 10992959..00000000 --- a/cli/src/query/assert.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::error; -use crate::error::Result; -use crate::test_highlight::parse_highlight_test; -use std::collections::hash_map::HashMap; -use std::fs; -use tree_sitter::{Language, Parser, Point}; - -#[derive(Debug, Eq, PartialEq)] -pub struct CaptureInfo { - pub name: String, - pub position: Point, -} - -pub fn assert_expected_captures( - infos: Vec, - path: String, - parser: &mut Parser, - language: Language, -) -> Result<()> { - let contents = fs::read_to_string(path)?; - let pairs = parse_highlight_test(parser, language, contents.as_bytes())?; - - let per_position_index: HashMap = - pairs.iter().map(|a| (a.position, &a.expected)).collect(); - - for info in &infos { - if !per_position_index.contains_key(&info.position) { - continue; - } - let found = per_position_index.get(&info.position).unwrap(); - if **found != info.name && info.name != "name" { - Err(error::Error::new(format!( - "Assertion failed: at {}, found {}, expected {}", - info.position, info.name, found - )))? - } - } - Ok(()) -} diff --git a/cli/src/query_testing.rs b/cli/src/query_testing.rs new file mode 100644 index 00000000..b6e2c169 --- /dev/null +++ b/cli/src/query_testing.rs @@ -0,0 +1,153 @@ +use crate::error; +use crate::error::Result; +use lazy_static::lazy_static; +use regex::Regex; +use std::collections::hash_map::HashMap; +use std::fs; +use tree_sitter::{Language, Parser, Point}; + +lazy_static! { + static ref HIGHLIGHT_NAME_REGEX: Regex = Regex::new("[\\w_\\-.]+").unwrap(); +} + +#[derive(Debug, Eq, PartialEq)] +pub struct CaptureInfo { + pub name: String, + pub position: Point, +} + +pub struct Assertion { + pub position: Point, + pub expected: String, +} + +/// Parse the given source code, finding all of the comments that contain +/// highlighting assertions. Return a vector of (position, expected highlight name) +/// pairs. +pub fn parse_highlight_test( + parser: &mut Parser, + language: Language, + source: &[u8], +) -> Result> { + let mut result = Vec::new(); + let mut assertion_ranges = Vec::new(); + + // Parse the code. + parser.set_included_ranges(&[]).unwrap(); + parser.set_language(language).unwrap(); + let tree = parser.parse(source, None).unwrap(); + + // Walk the tree, finding comment nodes that contain assertions. + let mut ascending = false; + let mut cursor = tree.root_node().walk(); + loop { + if ascending { + let node = cursor.node(); + + // Find every comment node. + if node.kind().contains("comment") { + if let Ok(text) = node.utf8_text(source) { + let mut position = node.start_position(); + if position.row == 0 { + continue; + } + + // Find the arrow character ("^" or '<-") in the comment. A left arrow + // refers to the column where the comment node starts. An up arrow refers + // to its own column. + let mut has_left_caret = false; + let mut has_arrow = false; + let mut arrow_end = 0; + for (i, c) in text.char_indices() { + arrow_end = i + 1; + if c == '-' && has_left_caret { + has_arrow = true; + break; + } + if c == '^' { + has_arrow = true; + position.column += i; + break; + } + has_left_caret = c == '<'; + } + + // If the comment node contains an arrow and a highlight name, record the + // highlight name and the position. + if let (true, Some(mat)) = + (has_arrow, HIGHLIGHT_NAME_REGEX.find(&text[arrow_end..])) + { + assertion_ranges.push((node.start_position(), node.end_position())); + result.push(Assertion { + position: position, + expected: mat.as_str().to_string(), + }); + } + } + } + + // Continue walking the tree. + if cursor.goto_next_sibling() { + ascending = false; + } else if !cursor.goto_parent() { + break; + } + } else if !cursor.goto_first_child() { + ascending = true; + } + } + + // Adjust the row number in each assertion's position to refer to the line of + // code *above* the assertion. There can be multiple lines of assertion comments, + // so the positions may have to be decremented by more than one row. + let mut i = 0; + for assertion in result.iter_mut() { + loop { + let on_assertion_line = assertion_ranges[i..] + .iter() + .any(|(start, _)| start.row == assertion.position.row); + if on_assertion_line { + assertion.position.row -= 1; + } else { + while i < assertion_ranges.len() + && assertion_ranges[i].0.row < assertion.position.row + { + i += 1; + } + break; + } + } + } + + // The assertions can end up out of order due to the line adjustments. + result.sort_unstable_by_key(|a| a.position); + + Ok(result) +} + +pub fn assert_expected_captures( + infos: Vec, + path: String, + parser: &mut Parser, + language: Language, +) -> Result<()> { + let contents = fs::read_to_string(path)?; + let pairs = parse_highlight_test(parser, language, contents.as_bytes())?; + + let per_position_index: HashMap = + pairs.iter().map(|a| (a.position, &a.expected)).collect(); + + for info in &infos { + if !per_position_index.contains_key(&info.position) { + continue; + } + let found = per_position_index.get(&info.position).unwrap(); + if **found != info.name && info.name != "name" { + Err(error::Error::new(format!( + "Assertion failed: at {}, found {}, expected {}", + info.position, found, info.name + )))? + } + } + Ok(()) +} diff --git a/cli/src/test_highlight.rs b/cli/src/test_highlight.rs index 45841a7b..da67f753 100644 --- a/cli/src/test_highlight.rs +++ b/cli/src/test_highlight.rs @@ -1,22 +1,12 @@ use super::error::Result; use crate::loader::Loader; +use crate::query_testing::{parse_highlight_test, Assertion}; use ansi_term::Colour; -use lazy_static::lazy_static; -use regex::Regex; use std::fs; use std::path::Path; -use tree_sitter::{Language, Parser, Point}; +use tree_sitter::Point; use tree_sitter_highlight::{Highlight, HighlightConfiguration, HighlightEvent, Highlighter}; -lazy_static! { - static ref HIGHLIGHT_NAME_REGEX: Regex = Regex::new("[\\w_\\-.]+").unwrap(); -} - -pub struct Assertion { - pub position: Point, - pub expected: String, -} - pub struct Failure { row: usize, column: usize, @@ -165,110 +155,6 @@ pub fn test_highlight( Ok(assertions.len()) } -/// Parse the given source code, finding all of the comments that contain -/// highlighting assertions. Return a vector of (position, expected highlight name) -/// pairs. -pub fn parse_highlight_test( - parser: &mut Parser, - language: Language, - source: &[u8], -) -> Result> { - let mut result = Vec::new(); - let mut assertion_ranges = Vec::new(); - - // Parse the code. - parser.set_included_ranges(&[]).unwrap(); - parser.set_language(language).unwrap(); - let tree = parser.parse(source, None).unwrap(); - - // Walk the tree, finding comment nodes that contain assertions. - let mut ascending = false; - let mut cursor = tree.root_node().walk(); - loop { - if ascending { - let node = cursor.node(); - - // Find every comment node. - if node.kind().contains("comment") { - if let Ok(text) = node.utf8_text(source) { - let mut position = node.start_position(); - if position.row == 0 { - continue; - } - - // Find the arrow character ("^" or '<-") in the comment. A left arrow - // refers to the column where the comment node starts. An up arrow refers - // to its own column. - let mut has_left_caret = false; - let mut has_arrow = false; - let mut arrow_end = 0; - for (i, c) in text.char_indices() { - arrow_end = i + 1; - if c == '-' && has_left_caret { - has_arrow = true; - break; - } - if c == '^' { - has_arrow = true; - position.column += i; - break; - } - has_left_caret = c == '<'; - } - - // If the comment node contains an arrow and a highlight name, record the - // highlight name and the position. - if let (true, Some(mat)) = - (has_arrow, HIGHLIGHT_NAME_REGEX.find(&text[arrow_end..])) - { - assertion_ranges.push((node.start_position(), node.end_position())); - result.push(Assertion { - position: position, - expected: mat.as_str().to_string(), - }); - } - } - } - - // Continue walking the tree. - if cursor.goto_next_sibling() { - ascending = false; - } else if !cursor.goto_parent() { - break; - } - } else if !cursor.goto_first_child() { - ascending = true; - } - } - - // Adjust the row number in each assertion's position to refer to the line of - // code *above* the assertion. There can be multiple lines of assertion comments, - // so the positions may have to be decremented by more than one row. - let mut i = 0; - for assertion in result.iter_mut() { - loop { - let on_assertion_line = assertion_ranges[i..] - .iter() - .any(|(start, _)| start.row == assertion.position.row); - if on_assertion_line { - assertion.position.row -= 1; - } else { - while i < assertion_ranges.len() - && assertion_ranges[i].0.row < assertion.position.row - { - i += 1; - } - break; - } - } - } - - // The assertions can end up out of order due to the line adjustments. - result.sort_unstable_by_key(|a| a.position); - - Ok(result) -} - pub fn get_highlight_positions( loader: &Loader, highlighter: &mut Highlighter,