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