1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
|
use std::collections::HashSet;
use std::fmt::{self, Display, Formatter};
use std::ops::Range;
use std::str::FromStr;
use ecow::EcoString;
use typst::syntax::{PackageVersion, Source};
use unscanny::Scanner;
/// Each test and subset may contain metadata.
#[derive(Debug)]
pub struct TestMetadata {
/// Configures how the test is run.
pub config: TestConfig,
/// Declares properties that must hold for a test.
///
/// For instance, `// Warning: 1-3 no text within underscores`
/// will fail the test if the warning isn't generated by your test.
pub annotations: HashSet<Annotation>,
}
/// Configuration of a test or subtest.
#[derive(Debug, Default)]
pub struct TestConfig {
/// Reference images will be generated and compared.
///
/// Defaults to `true`, can be disabled with `Ref: false`.
pub compare_ref: Option<bool>,
/// Hint annotations will be compared to compiler hints.
///
/// Defaults to `true`, can be disabled with `Hints: false`.
pub validate_hints: Option<bool>,
/// Autocompletion annotations will be validated against autocompletions.
/// Mutually exclusive with error and hint annotations.
///
/// Defaults to `false`, can be enabled with `Autocomplete: true`.
pub validate_autocomplete: Option<bool>,
}
/// Parsing error when the metadata is invalid.
pub(crate) enum InvalidMetadata {
/// An invalid annotation and it's error message.
InvalidAnnotation(Annotation, String),
/// Setting metadata can only be done with `true` or `false` as a value.
InvalidSet(String),
}
impl InvalidMetadata {
pub(crate) fn write(
invalid_data: Vec<InvalidMetadata>,
output: &mut String,
print_annotation: &mut impl FnMut(&Annotation, &mut String),
) {
use std::fmt::Write;
for data in invalid_data.into_iter() {
let (annotation, error) = match data {
InvalidMetadata::InvalidAnnotation(a, e) => (Some(a), e),
InvalidMetadata::InvalidSet(e) => (None, e),
};
write!(output, "{error}",).unwrap();
if let Some(annotation) = annotation {
print_annotation(&annotation, output)
} else {
writeln!(output).unwrap();
}
}
}
}
/// Annotation of the form `// KIND: RANGE TEXT`.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Annotation {
/// Which kind of annotation this is.
pub kind: AnnotationKind,
/// May be written as:
/// - `{line}:{col}-{line}:{col}`, e.g. `0:4-0:6`.
/// - `{col}-{col}`, e.g. `4-6`:
/// The line is assumed to be the line after the annotation.
/// - `-1`: Produces a range of length zero at the end of the next line.
/// Mostly useful for autocompletion tests which require an index.
pub range: Option<Range<usize>>,
/// The raw text after the annotation.
pub text: EcoString,
}
/// The different kinds of in-test annotations.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum AnnotationKind {
Error,
Warning,
Hint,
AutocompleteContains,
AutocompleteExcludes,
}
impl AnnotationKind {
/// Returns the user-facing string for this annotation.
pub fn as_str(self) -> &'static str {
match self {
AnnotationKind::Error => "Error",
AnnotationKind::Warning => "Warning",
AnnotationKind::Hint => "Hint",
AnnotationKind::AutocompleteContains => "Autocomplete contains",
AnnotationKind::AutocompleteExcludes => "Autocomplete excludes",
}
}
}
impl FromStr for AnnotationKind {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"Error" => AnnotationKind::Error,
"Warning" => AnnotationKind::Warning,
"Hint" => AnnotationKind::Hint,
"Autocomplete contains" => AnnotationKind::AutocompleteContains,
"Autocomplete excludes" => AnnotationKind::AutocompleteExcludes,
_ => return Err("invalid annotatino"),
})
}
}
impl Display for AnnotationKind {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad(self.as_str())
}
}
/// Parse metadata for a test.
pub fn parse_part_metadata(
source: &Source,
is_header: bool,
) -> Result<TestMetadata, Vec<InvalidMetadata>> {
let mut config = TestConfig::default();
let mut annotations = HashSet::default();
let mut invalid_data = vec![];
let lines = source_to_lines(source);
for (i, line) in lines.iter().enumerate() {
if let Some((key, value)) = parse_metadata_line(line) {
let key = key.trim();
match key {
"Ref" => validate_set_annotation(
value,
&mut config.compare_ref,
&mut invalid_data,
),
"Hints" => validate_set_annotation(
value,
&mut config.validate_hints,
&mut invalid_data,
),
"Autocomplete" => validate_set_annotation(
value,
&mut config.validate_autocomplete,
&mut invalid_data,
),
annotation_key => {
let Ok(kind) = AnnotationKind::from_str(annotation_key) else {
continue;
};
let mut s = Scanner::new(value);
let range = parse_range(&mut s, i, source);
let rest = if range.is_some() { s.after() } else { s.string() };
let message = rest
.trim()
.replace("VERSION", &PackageVersion::compiler().to_string())
.into();
let annotation =
Annotation { kind, range: range.clone(), text: message };
if is_header {
invalid_data.push(InvalidMetadata::InvalidAnnotation(
annotation,
format!(
"Error: header may not contain annotations of type {kind}"
),
));
continue;
}
if matches!(
kind,
AnnotationKind::AutocompleteContains
| AnnotationKind::AutocompleteExcludes
) {
if let Some(range) = range {
if range.start != range.end {
invalid_data.push(InvalidMetadata::InvalidAnnotation(
annotation,
"Error: found range in Autocomplete annotation where range.start != range.end, range.end would be ignored."
.to_string()
));
continue;
}
} else {
invalid_data.push(InvalidMetadata::InvalidAnnotation(
annotation,
"Error: autocomplete annotation but no range specified"
.to_string(),
));
continue;
}
}
annotations.insert(annotation);
}
}
}
}
if invalid_data.is_empty() {
Ok(TestMetadata { config, annotations })
} else {
Err(invalid_data)
}
}
/// Extract key and value for a metadata line of the form: `// KEY: VALUE`.
fn parse_metadata_line(line: &str) -> Option<(&str, &str)> {
let mut s = Scanner::new(line);
if !s.eat_if("// ") {
return None;
}
let key = s.eat_until(':').trim();
if !s.eat_if(':') {
return None;
}
let value = s.eat_until('\n').trim();
Some((key, value))
}
/// Parse a quoted string.
fn parse_string<'a>(s: &mut Scanner<'a>) -> Option<&'a str> {
if !s.eat_if('"') {
return None;
}
let sub = s.eat_until('"');
if !s.eat_if('"') {
return None;
}
Some(sub)
}
/// Parse a number.
fn parse_num(s: &mut Scanner) -> Option<isize> {
let mut first = true;
let n = &s.eat_while(|c: char| {
let valid = first && c == '-' || c.is_numeric();
first = false;
valid
});
n.parse().ok()
}
/// Parse a comma-separated list of strings.
pub fn parse_string_list(text: &str) -> HashSet<&str> {
let mut s = Scanner::new(text);
let mut result = HashSet::new();
while let Some(sub) = parse_string(&mut s) {
result.insert(sub);
s.eat_whitespace();
if !s.eat_if(',') {
break;
}
s.eat_whitespace();
}
result
}
/// Parse a position.
fn parse_pos(s: &mut Scanner, i: usize, source: &Source) -> Option<usize> {
let first = parse_num(s)? - 1;
let (delta, column) =
if s.eat_if(':') { (first, parse_num(s)? - 1) } else { (0, first) };
let line = (i + comments_until_code(source, i)).checked_add_signed(delta)?;
source.line_column_to_byte(line, usize::try_from(column).ok()?)
}
/// Parse a range.
fn parse_range(s: &mut Scanner, i: usize, source: &Source) -> Option<Range<usize>> {
let lines = source_to_lines(source);
s.eat_whitespace();
if s.eat_if("-1") {
let mut add = 1;
while let Some(line) = lines.get(i + add) {
if !line.starts_with("//") {
break;
}
add += 1;
}
let next_line = lines.get(i + add)?;
let col = next_line.chars().count();
let index = source.line_column_to_byte(i + add, col)?;
s.eat_whitespace();
return Some(index..index);
}
let start = parse_pos(s, i, source)?;
let end = if s.eat_if('-') { parse_pos(s, i, source)? } else { start };
s.eat_whitespace();
Some(start..end)
}
/// Returns the number of lines of comment from line i to next line of code.
fn comments_until_code(source: &Source, i: usize) -> usize {
source_to_lines(source)[i..]
.iter()
.take_while(|line| line.starts_with("//"))
.count()
}
fn source_to_lines(source: &Source) -> Vec<&str> {
source.text().lines().map(str::trim).collect()
}
fn validate_set_annotation(
value: &str,
flag: &mut Option<bool>,
invalid_data: &mut Vec<InvalidMetadata>,
) {
let value = value.trim();
if value != "false" && value != "true" {
invalid_data.push(
InvalidMetadata::InvalidSet(format!("Error: trying to set Ref, Hints, or Autocomplete with value {value:?} != true, != false.")))
} else {
*flag = Some(value == "true")
}
}
|