1 use crate::query_testing::{parse_position_comments, Assertion};
2 use ansi_term::Colour;
3 use anyhow::{anyhow, Result};
4 use std::fs;
5 use std::path::Path;
6 use tree_sitter::Point;
7 use tree_sitter_highlight::{Highlight, HighlightConfiguration, HighlightEvent, Highlighter};
8 use tree_sitter_loader::Loader;
9
10 #[derive(Debug)]
11 pub struct Failure {
12 row: usize,
13 column: usize,
14 expected_highlight: String,
15 actual_highlights: Vec<String>,
16 }
17
18 impl std::error::Error for Failure {}
19
20 impl std::fmt::Display for Failure {
fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result21 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
22 write!(
23 f,
24 "Failure - row: {}, column: {}, expected highlight '{}', actual highlights: ",
25 self.row, self.column, self.expected_highlight
26 )?;
27 if self.actual_highlights.is_empty() {
28 write!(f, "none.")?;
29 } else {
30 for (i, actual_highlight) in self.actual_highlights.iter().enumerate() {
31 if i > 0 {
32 write!(f, ", ")?;
33 }
34 write!(f, "'{}'", actual_highlight)?;
35 }
36 }
37 Ok(())
38 }
39 }
40
test_highlights(loader: &Loader, directory: &Path) -> Result<()>41 pub fn test_highlights(loader: &Loader, directory: &Path) -> Result<()> {
42 let mut failed = false;
43 let mut highlighter = Highlighter::new();
44
45 println!("syntax highlighting:");
46 for highlight_test_file in fs::read_dir(directory)? {
47 let highlight_test_file = highlight_test_file?;
48 let test_file_path = highlight_test_file.path();
49 let test_file_name = highlight_test_file.file_name();
50 let (language, language_config) = loader
51 .language_configuration_for_file_name(&test_file_path)?
52 .ok_or_else(|| anyhow!("No language found for path {:?}", test_file_path))?;
53 let highlight_config = language_config
54 .highlight_config(language)?
55 .ok_or_else(|| anyhow!("No highlighting config found for {:?}", test_file_path))?;
56 match test_highlight(
57 &loader,
58 &mut highlighter,
59 highlight_config,
60 fs::read(&test_file_path)?.as_slice(),
61 ) {
62 Ok(assertion_count) => {
63 println!(
64 " ✓ {} ({} assertions)",
65 Colour::Green.paint(test_file_name.to_string_lossy().as_ref()),
66 assertion_count
67 );
68 }
69 Err(e) => {
70 println!(
71 " ✗ {}",
72 Colour::Red.paint(test_file_name.to_string_lossy().as_ref())
73 );
74 println!(" {}", e);
75 failed = true;
76 }
77 }
78 }
79
80 if failed {
81 Err(anyhow!(""))
82 } else {
83 Ok(())
84 }
85 }
iterate_assertions( assertions: &Vec<Assertion>, highlights: &Vec<(Point, Point, Highlight)>, highlight_names: &Vec<String>, ) -> Result<usize>86 pub fn iterate_assertions(
87 assertions: &Vec<Assertion>,
88 highlights: &Vec<(Point, Point, Highlight)>,
89 highlight_names: &Vec<String>,
90 ) -> Result<usize> {
91 // Iterate through all of the highlighting assertions, checking each one against the
92 // actual highlights.
93 let mut i = 0;
94 let mut actual_highlights = Vec::<&String>::new();
95 for Assertion {
96 position,
97 expected_capture_name: expected_highlight,
98 } in assertions
99 {
100 let mut passed = false;
101 actual_highlights.clear();
102
103 'highlight_loop: loop {
104 // The assertions are ordered by position, so skip past all of the highlights that
105 // end at or before this assertion's position.
106 if let Some(highlight) = highlights.get(i) {
107 if highlight.1 <= *position {
108 i += 1;
109 continue;
110 }
111
112 // Iterate through all of the highlights that start at or before this assertion's,
113 // position, looking for one that matches the assertion.
114 let mut j = i;
115 while let (false, Some(highlight)) = (passed, highlights.get(j)) {
116 if highlight.0 > *position {
117 break 'highlight_loop;
118 }
119
120 // If the highlight matches the assertion, this test passes. Otherwise,
121 // add this highlight to the list of actual highlights that span the
122 // assertion's position, in order to generate an error message in the event
123 // of a failure.
124 let highlight_name = &highlight_names[(highlight.2).0];
125 if *highlight_name == *expected_highlight {
126 passed = true;
127 break 'highlight_loop;
128 } else {
129 actual_highlights.push(highlight_name);
130 }
131
132 j += 1;
133 }
134 } else {
135 break;
136 }
137 }
138
139 if !passed {
140 return Err(Failure {
141 row: position.row,
142 column: position.column,
143 expected_highlight: expected_highlight.clone(),
144 actual_highlights: actual_highlights.into_iter().cloned().collect(),
145 }
146 .into());
147 }
148 }
149
150 Ok(assertions.len())
151 }
152
test_highlight( loader: &Loader, highlighter: &mut Highlighter, highlight_config: &HighlightConfiguration, source: &[u8], ) -> Result<usize>153 pub fn test_highlight(
154 loader: &Loader,
155 highlighter: &mut Highlighter,
156 highlight_config: &HighlightConfiguration,
157 source: &[u8],
158 ) -> Result<usize> {
159 // Highlight the file, and parse out all of the highlighting assertions.
160 let highlight_names = loader.highlight_names();
161 let highlights = get_highlight_positions(loader, highlighter, highlight_config, source)?;
162 let assertions =
163 parse_position_comments(highlighter.parser(), highlight_config.language, source)?;
164
165 iterate_assertions(&assertions, &highlights, &highlight_names)?;
166
167 // Iterate through all of the highlighting assertions, checking each one against the
168 // actual highlights.
169 let mut i = 0;
170 let mut actual_highlights = Vec::<&String>::new();
171 for Assertion {
172 position,
173 expected_capture_name: expected_highlight,
174 } in &assertions
175 {
176 let mut passed = false;
177 actual_highlights.clear();
178
179 'highlight_loop: loop {
180 // The assertions are ordered by position, so skip past all of the highlights that
181 // end at or before this assertion's position.
182 if let Some(highlight) = highlights.get(i) {
183 if highlight.1 <= *position {
184 i += 1;
185 continue;
186 }
187
188 // Iterate through all of the highlights that start at or before this assertion's,
189 // position, looking for one that matches the assertion.
190 let mut j = i;
191 while let (false, Some(highlight)) = (passed, highlights.get(j)) {
192 if highlight.0 > *position {
193 break 'highlight_loop;
194 }
195
196 // If the highlight matches the assertion, this test passes. Otherwise,
197 // add this highlight to the list of actual highlights that span the
198 // assertion's position, in order to generate an error message in the event
199 // of a failure.
200 let highlight_name = &highlight_names[(highlight.2).0];
201 if *highlight_name == *expected_highlight {
202 passed = true;
203 break 'highlight_loop;
204 } else {
205 actual_highlights.push(highlight_name);
206 }
207
208 j += 1;
209 }
210 } else {
211 break;
212 }
213 }
214
215 if !passed {
216 return Err(Failure {
217 row: position.row,
218 column: position.column,
219 expected_highlight: expected_highlight.clone(),
220 actual_highlights: actual_highlights.into_iter().cloned().collect(),
221 }
222 .into());
223 }
224 }
225
226 Ok(assertions.len())
227 }
228
get_highlight_positions( loader: &Loader, highlighter: &mut Highlighter, highlight_config: &HighlightConfiguration, source: &[u8], ) -> Result<Vec<(Point, Point, Highlight)>>229 pub fn get_highlight_positions(
230 loader: &Loader,
231 highlighter: &mut Highlighter,
232 highlight_config: &HighlightConfiguration,
233 source: &[u8],
234 ) -> Result<Vec<(Point, Point, Highlight)>> {
235 let mut row = 0;
236 let mut column = 0;
237 let mut byte_offset = 0;
238 let mut was_newline = false;
239 let mut result = Vec::new();
240 let mut highlight_stack = Vec::new();
241 let source = String::from_utf8_lossy(source);
242 let mut char_indices = source.char_indices();
243 for event in highlighter.highlight(highlight_config, source.as_bytes(), None, |string| {
244 loader.highlight_config_for_injection_string(string)
245 })? {
246 match event? {
247 HighlightEvent::HighlightStart(h) => highlight_stack.push(h),
248 HighlightEvent::HighlightEnd => {
249 highlight_stack.pop();
250 }
251 HighlightEvent::Source { start, end } => {
252 let mut start_position = Point::new(row, column);
253 while byte_offset < end {
254 if byte_offset <= start {
255 start_position = Point::new(row, column);
256 }
257 if let Some((i, c)) = char_indices.next() {
258 if was_newline {
259 row += 1;
260 column = 0;
261 } else {
262 column += i - byte_offset;
263 }
264 was_newline = c == '\n';
265 byte_offset = i;
266 } else {
267 break;
268 }
269 }
270 if let Some(highlight) = highlight_stack.last() {
271 result.push((start_position, Point::new(row, column), *highlight))
272 }
273 }
274 }
275 }
276 Ok(result)
277 }
278