1 | use std::{ |
2 | collections::HashMap, |
3 | num::NonZeroUsize, |
4 | path::{Path, PathBuf}, |
5 | process::Command, |
6 | }; |
7 | |
8 | use bstr::{ByteSlice, Utf8Error}; |
9 | use regex::bytes::Regex; |
10 | |
11 | use crate::{ |
12 | rustc_stderr::{Level, Span}, |
13 | Error, Errored, Mode, |
14 | }; |
15 | |
16 | use color_eyre::eyre::{Context, Result}; |
17 | |
18 | pub(crate) use spanned::*; |
19 | |
20 | mod spanned; |
21 | #[cfg (test)] |
22 | mod tests; |
23 | |
24 | /// This crate supports various magic comments that get parsed as file-specific |
25 | /// configuration values. This struct parses them all in one go and then they |
26 | /// get processed by their respective use sites. |
27 | #[derive (Default, Debug)] |
28 | pub(crate) struct Comments { |
29 | /// List of revision names to execute. Can only be specified once |
30 | pub revisions: Option<Vec<String>>, |
31 | /// Comments that are only available under specific revisions. |
32 | /// The defaults are in key `vec![]` |
33 | pub revisioned: HashMap<Vec<String>, Revisioned>, |
34 | } |
35 | |
36 | impl Comments { |
37 | /// Check that a comment isn't specified twice across multiple differently revisioned statements. |
38 | /// e.g. `//@[foo, bar] error-in-other-file: bop` and `//@[foo, baz] error-in-other-file boop` would end up |
39 | /// specifying two error patterns that are available in revision `foo`. |
40 | pub fn find_one_for_revision<'a, T: 'a>( |
41 | &'a self, |
42 | revision: &'a str, |
43 | kind: &str, |
44 | f: impl Fn(&'a Revisioned) -> OptWithLine<T>, |
45 | ) -> Result<OptWithLine<T>, Errored> { |
46 | let mut result = None; |
47 | let mut errors = vec![]; |
48 | for rev in self.for_revision(revision) { |
49 | if let Some(found) = f(rev).into_inner() { |
50 | if result.is_some() { |
51 | errors.push(found.line()); |
52 | } else { |
53 | result = found.into(); |
54 | } |
55 | } |
56 | } |
57 | if errors.is_empty() { |
58 | Ok(result.into()) |
59 | } else { |
60 | Err(Errored { |
61 | command: Command::new(format!("<finding flags for revision ` {revision}`>" )), |
62 | errors: vec![Error::MultipleRevisionsWithResults { |
63 | kind: kind.to_string(), |
64 | lines: errors, |
65 | }], |
66 | stderr: vec![], |
67 | stdout: vec![], |
68 | }) |
69 | } |
70 | } |
71 | |
72 | /// Returns an iterator over all revisioned comments that match the revision. |
73 | pub fn for_revision<'a>(&'a self, revision: &'a str) -> impl Iterator<Item = &'a Revisioned> { |
74 | self.revisioned.iter().filter_map(move |(k, v)| { |
75 | if k.is_empty() || k.iter().any(|rev| rev == revision) { |
76 | Some(v) |
77 | } else { |
78 | None |
79 | } |
80 | }) |
81 | } |
82 | |
83 | pub(crate) fn edition( |
84 | &self, |
85 | revision: &str, |
86 | config: &crate::Config, |
87 | ) -> Result<Option<MaybeSpanned<String>>, Errored> { |
88 | let edition = |
89 | self.find_one_for_revision(revision, "`edition` annotations" , |r| r.edition.clone())?; |
90 | let edition = edition |
91 | .into_inner() |
92 | .map(MaybeSpanned::from) |
93 | .or(config.edition.clone().map(MaybeSpanned::new_config)); |
94 | Ok(edition) |
95 | } |
96 | } |
97 | |
98 | #[derive (Debug)] |
99 | /// Comments that can be filtered for specific revisions. |
100 | pub(crate) struct Revisioned { |
101 | /// The character range in which this revisioned item was first added. |
102 | /// Used for reporting errors on unknown revisions. |
103 | pub span: Span, |
104 | /// Don't run this test if any of these filters apply |
105 | pub ignore: Vec<Condition>, |
106 | /// Only run this test if all of these filters apply |
107 | pub only: Vec<Condition>, |
108 | /// Generate one .stderr file per bit width, by prepending with `.64bit` and similar |
109 | pub stderr_per_bitwidth: bool, |
110 | /// Additional flags to pass to the executable |
111 | pub compile_flags: Vec<String>, |
112 | /// Additional env vars to set for the executable |
113 | pub env_vars: Vec<(String, String)>, |
114 | /// Normalizations to apply to the stderr output before emitting it to disk |
115 | pub normalize_stderr: Vec<(Regex, Vec<u8>)>, |
116 | /// Normalizations to apply to the stdout output before emitting it to disk |
117 | pub normalize_stdout: Vec<(Regex, Vec<u8>)>, |
118 | /// Arbitrary patterns to look for in the stderr. |
119 | /// The error must be from another file, as errors from the current file must be |
120 | /// checked via `error_matches`. |
121 | pub error_in_other_files: Vec<Spanned<Pattern>>, |
122 | pub error_matches: Vec<ErrorMatch>, |
123 | /// Ignore diagnostics below this level. |
124 | /// `None` means pick the lowest level from the `error_pattern`s. |
125 | pub require_annotations_for_level: OptWithLine<Level>, |
126 | pub aux_builds: Vec<Spanned<PathBuf>>, |
127 | pub edition: OptWithLine<String>, |
128 | /// Overwrites the mode from `Config`. |
129 | pub mode: OptWithLine<Mode>, |
130 | pub needs_asm_support: bool, |
131 | /// Don't run [`rustfix`] for this test |
132 | pub no_rustfix: OptWithLine<()>, |
133 | } |
134 | |
135 | #[derive (Debug)] |
136 | struct CommentParser<T> { |
137 | /// The comments being built. |
138 | comments: T, |
139 | /// Any errors that ocurred during comment parsing. |
140 | errors: Vec<Error>, |
141 | /// The available commands and their parsing logic |
142 | commands: HashMap<&'static str, CommandParserFunc>, |
143 | } |
144 | |
145 | type CommandParserFunc = fn(&mut CommentParser<&mut Revisioned>, args: Spanned<&str>, span: Span); |
146 | |
147 | impl<T> std::ops::Deref for CommentParser<T> { |
148 | type Target = T; |
149 | |
150 | fn deref(&self) -> &Self::Target { |
151 | &self.comments |
152 | } |
153 | } |
154 | |
155 | impl<T> std::ops::DerefMut for CommentParser<T> { |
156 | fn deref_mut(&mut self) -> &mut Self::Target { |
157 | &mut self.comments |
158 | } |
159 | } |
160 | |
161 | /// The conditions used for "ignore" and "only" filters. |
162 | #[derive (Debug)] |
163 | pub(crate) enum Condition { |
164 | /// The given string must appear in the host triple. |
165 | Host(String), |
166 | /// The given string must appear in the target triple. |
167 | Target(String), |
168 | /// Tests that the bitwidth is the given one. |
169 | Bitwidth(u8), |
170 | /// Tests that the target is the host. |
171 | OnHost, |
172 | } |
173 | |
174 | #[derive (Debug, Clone)] |
175 | /// An error pattern parsed from a `//~` comment. |
176 | pub enum Pattern { |
177 | SubString(String), |
178 | Regex(Regex), |
179 | } |
180 | |
181 | #[derive (Debug)] |
182 | pub(crate) struct ErrorMatch { |
183 | pub pattern: Spanned<Pattern>, |
184 | pub level: Level, |
185 | /// The line this pattern is expecting to find a message in. |
186 | pub line: NonZeroUsize, |
187 | } |
188 | |
189 | impl Condition { |
190 | fn parse(c: &str) -> std::result::Result<Self, String> { |
191 | if c == "on-host" { |
192 | Ok(Condition::OnHost) |
193 | } else if let Some(bits: &str) = c.strip_suffix("bit" ) { |
194 | let bits: u8 = bits.parse().map_err(|_err: ParseIntError| { |
195 | format!("invalid ignore/only filter ending in 'bit': {c:?} is not a valid bitwdith" ) |
196 | })?; |
197 | Ok(Condition::Bitwidth(bits)) |
198 | } else if let Some(triple_substr: &str) = c.strip_prefix("target-" ) { |
199 | Ok(Condition::Target(triple_substr.to_owned())) |
200 | } else if let Some(triple_substr: &str) = c.strip_prefix("host-" ) { |
201 | Ok(Condition::Host(triple_substr.to_owned())) |
202 | } else { |
203 | Err(format!( |
204 | "` {c}` is not a valid condition, expected `on-host`, /[0-9]+bit/, /host-.*/, or /target-.*/" |
205 | )) |
206 | } |
207 | } |
208 | } |
209 | |
210 | impl Comments { |
211 | pub(crate) fn parse_file(path: &Path) -> Result<std::result::Result<Self, Vec<Error>>> { |
212 | let content = |
213 | std::fs::read(path).wrap_err_with(|| format!("failed to read {}" , path.display()))?; |
214 | Ok(Self::parse(&content)) |
215 | } |
216 | |
217 | /// Parse comments in `content`. |
218 | /// `path` is only used to emit diagnostics if parsing fails. |
219 | pub(crate) fn parse( |
220 | content: &(impl AsRef<[u8]> + ?Sized), |
221 | ) -> std::result::Result<Self, Vec<Error>> { |
222 | let mut parser = CommentParser { |
223 | comments: Comments::default(), |
224 | errors: vec![], |
225 | commands: CommentParser::<_>::commands(), |
226 | }; |
227 | |
228 | let mut fallthrough_to = None; // The line that a `|` will refer to. |
229 | for (l, line) in content.as_ref().lines().enumerate() { |
230 | let l = NonZeroUsize::new(l + 1).unwrap(); // enumerate starts at 0, but line numbers start at 1 |
231 | let span = Span { |
232 | line_start: l, |
233 | line_end: l, |
234 | column_start: NonZeroUsize::new(1).unwrap(), |
235 | column_end: NonZeroUsize::new(line.chars().count() + 1).unwrap(), |
236 | }; |
237 | match parser.parse_checked_line(&mut fallthrough_to, Spanned::new(line, span)) { |
238 | Ok(()) => {} |
239 | Err(e) => parser.error(span, format!("Comment is not utf8: {e:?}" )), |
240 | } |
241 | } |
242 | if let Some(revisions) = &parser.comments.revisions { |
243 | for (key, revisioned) in &parser.comments.revisioned { |
244 | for rev in key { |
245 | if !revisions.contains(rev) { |
246 | parser.errors.push(Error::InvalidComment { |
247 | msg: format!("the revision ` {rev}` is not known" ), |
248 | span: revisioned.span, |
249 | }) |
250 | } |
251 | } |
252 | } |
253 | } else { |
254 | for (key, revisioned) in &parser.comments.revisioned { |
255 | if !key.is_empty() { |
256 | parser.errors.push(Error::InvalidComment { |
257 | msg: "there are no revisions in this test" .into(), |
258 | span: revisioned.span, |
259 | }) |
260 | } |
261 | } |
262 | } |
263 | if parser.errors.is_empty() { |
264 | Ok(parser.comments) |
265 | } else { |
266 | Err(parser.errors) |
267 | } |
268 | } |
269 | } |
270 | |
271 | impl CommentParser<Comments> { |
272 | fn parse_checked_line( |
273 | &mut self, |
274 | fallthrough_to: &mut Option<NonZeroUsize>, |
275 | line: Spanned<&[u8]>, |
276 | ) -> std::result::Result<(), Utf8Error> { |
277 | if let Some(command) = line.strip_prefix(b"//@" ) { |
278 | self.parse_command(command.to_str()?.trim()) |
279 | } else if let Some((_, pattern)) = line.split_once_str("//~" ) { |
280 | let (revisions, pattern) = self.parse_revisions(pattern.to_str()?); |
281 | self.revisioned(revisions, |this| { |
282 | this.parse_pattern(pattern, fallthrough_to) |
283 | }) |
284 | } else { |
285 | *fallthrough_to = None; |
286 | for pos in line.find_iter("//" ) { |
287 | let (_, rest) = line.to_str()?.split_at(pos + 2); |
288 | for rest in std::iter::once(rest).chain(rest.strip_prefix(" " )) { |
289 | if let Some('@' | '~' | '[' | ']' | '^' | '|' ) = rest.chars().next() { |
290 | self.error( |
291 | rest.span(), |
292 | format!( |
293 | "comment looks suspiciously like a test suite command: ` {}` \n\ |
294 | All `//@` test suite commands must be at the start of the line. \n\ |
295 | The `//` must be directly followed by `@` or `~`." , |
296 | *rest, |
297 | ), |
298 | ); |
299 | } else { |
300 | let mut parser = Self { |
301 | errors: vec![], |
302 | comments: Comments::default(), |
303 | commands: std::mem::take(&mut self.commands), |
304 | }; |
305 | parser.parse_command(rest); |
306 | if parser.errors.is_empty() { |
307 | self.error( |
308 | rest.span(), |
309 | "a compiletest-rs style comment was detected. \n\ |
310 | Please use text that could not also be interpreted as a command, \n\ |
311 | and prefix all actual commands with `//@`" , |
312 | ); |
313 | } |
314 | self.commands = parser.commands; |
315 | } |
316 | } |
317 | } |
318 | } |
319 | Ok(()) |
320 | } |
321 | } |
322 | |
323 | impl<CommentsType> CommentParser<CommentsType> { |
324 | fn error(&mut self, span: Span, s: impl Into<String>) { |
325 | self.errors.push(Error::InvalidComment { |
326 | msg: s.into(), |
327 | span, |
328 | }); |
329 | } |
330 | |
331 | fn check(&mut self, span: Span, cond: bool, s: impl Into<String>) { |
332 | if !cond { |
333 | self.error(span, s); |
334 | } |
335 | } |
336 | |
337 | fn check_some<T>(&mut self, span: Span, opt: Option<T>, s: impl Into<String>) -> Option<T> { |
338 | self.check(span, cond:opt.is_some(), s); |
339 | opt |
340 | } |
341 | } |
342 | |
343 | impl CommentParser<Comments> { |
344 | fn parse_command(&mut self, command: Spanned<&str>) { |
345 | let (revisions, command) = self.parse_revisions(command); |
346 | |
347 | // Commands are letters or dashes, grab everything until the first character that is neither of those. |
348 | let (command, args) = match command |
349 | .char_indices() |
350 | .find_map(|(i, c)| (!c.is_alphanumeric() && c != '-' && c != '_' ).then_some(i)) |
351 | { |
352 | None => (command, Spanned::new("" , command.span().shrink_to_end())), |
353 | Some(i) => { |
354 | let (command, args) = command.split_at(i); |
355 | // Commands are separated from their arguments by ':' or ' ' |
356 | let next = args |
357 | .chars() |
358 | .next() |
359 | .expect("the `position` above guarantees that there is at least one char" ); |
360 | self.check( |
361 | args.span().shrink_to_start(), |
362 | next == ':' , |
363 | "test command must be followed by `:` (or end the line)" , |
364 | ); |
365 | (command, args.split_at(next.len_utf8()).1.trim()) |
366 | } |
367 | }; |
368 | |
369 | if *command == "revisions" { |
370 | self.check( |
371 | revisions.span(), |
372 | revisions.is_empty(), |
373 | "revisions cannot be declared under a revision" , |
374 | ); |
375 | self.check( |
376 | revisions.span(), |
377 | self.revisions.is_none(), |
378 | "cannot specify `revisions` twice" , |
379 | ); |
380 | self.revisions = Some(args.split_whitespace().map(|s| s.to_string()).collect()); |
381 | return; |
382 | } |
383 | self.revisioned(revisions, |this| this.parse_command(command, args)); |
384 | } |
385 | |
386 | fn revisioned( |
387 | &mut self, |
388 | revisions: Spanned<Vec<String>>, |
389 | f: impl FnOnce(&mut CommentParser<&mut Revisioned>), |
390 | ) { |
391 | let span = revisions.span(); |
392 | let revisions = revisions.into_inner(); |
393 | let mut this = CommentParser { |
394 | errors: std::mem::take(&mut self.errors), |
395 | commands: std::mem::take(&mut self.commands), |
396 | comments: self |
397 | .revisioned |
398 | .entry(revisions) |
399 | .or_insert_with(|| Revisioned { |
400 | span, |
401 | ignore: Default::default(), |
402 | only: Default::default(), |
403 | stderr_per_bitwidth: Default::default(), |
404 | compile_flags: Default::default(), |
405 | env_vars: Default::default(), |
406 | normalize_stderr: Default::default(), |
407 | normalize_stdout: Default::default(), |
408 | error_in_other_files: Default::default(), |
409 | error_matches: Default::default(), |
410 | require_annotations_for_level: Default::default(), |
411 | aux_builds: Default::default(), |
412 | edition: Default::default(), |
413 | mode: Default::default(), |
414 | needs_asm_support: Default::default(), |
415 | no_rustfix: Default::default(), |
416 | }), |
417 | }; |
418 | f(&mut this); |
419 | let CommentParser { |
420 | errors, commands, .. |
421 | } = this; |
422 | self.commands = commands; |
423 | self.errors = errors; |
424 | } |
425 | } |
426 | |
427 | impl CommentParser<&mut Revisioned> { |
428 | fn parse_normalize_test( |
429 | &mut self, |
430 | args: Spanned<&str>, |
431 | mode: &str, |
432 | ) -> Option<(Regex, Vec<u8>)> { |
433 | let (from, rest) = self.parse_str(args); |
434 | |
435 | let to = match rest.strip_prefix("->" ) { |
436 | Some(v) => v, |
437 | None => { |
438 | self.error( |
439 | rest.span(), |
440 | format!( |
441 | "normalize- {mode}-test needs a pattern and replacement separated by `->`" |
442 | ), |
443 | ); |
444 | return None; |
445 | } |
446 | } |
447 | .trim_start(); |
448 | let (to, rest) = self.parse_str(to); |
449 | |
450 | self.check( |
451 | rest.span(), |
452 | rest.is_empty(), |
453 | "trailing text after pattern replacement" , |
454 | ); |
455 | |
456 | let regex = self.parse_regex(from)?; |
457 | Some((regex, to.as_bytes().to_owned())) |
458 | } |
459 | |
460 | fn commands() -> HashMap<&'static str, CommandParserFunc> { |
461 | let mut commands = HashMap::<_, CommandParserFunc>::new(); |
462 | macro_rules! commands { |
463 | ($($name:expr => ($this:ident, $args:ident, $span:ident)$block:block)*) => { |
464 | $(commands.insert($name, |$this, $args, $span| { |
465 | $block |
466 | });)* |
467 | }; |
468 | } |
469 | commands! { |
470 | "compile-flags" => (this, args, _span){ |
471 | if let Some(parsed) = comma::parse_command(*args) { |
472 | this.compile_flags.extend(parsed); |
473 | } else { |
474 | this.error(args.span(), format!("` {}` contains an unclosed quotation mark" , *args)); |
475 | } |
476 | } |
477 | "rustc-env" => (this, args, _span){ |
478 | for env in args.split_whitespace() { |
479 | if let Some((k, v)) = this.check_some( |
480 | args.span(), |
481 | env.split_once('=' ), |
482 | "environment variables must be key/value pairs separated by a `=`" , |
483 | ) { |
484 | this.env_vars.push((k.to_string(), v.to_string())); |
485 | } |
486 | } |
487 | } |
488 | "normalize-stderr-test" => (this, args, _span){ |
489 | if let Some(res) = this.parse_normalize_test(args, "stderr" ) { |
490 | this.normalize_stderr.push(res) |
491 | } |
492 | } |
493 | "normalize-stdout-test" => (this, args, _span){ |
494 | if let Some(res) = this.parse_normalize_test(args, "stdout" ) { |
495 | this.normalize_stdout.push(res) |
496 | } |
497 | } |
498 | "error-pattern" => (this, _args, span){ |
499 | this.error(span, "`error-pattern` has been renamed to `error-in-other-file`" ); |
500 | } |
501 | "error-in-other-file" => (this, args, _span){ |
502 | let args = args.trim(); |
503 | let pat = this.parse_error_pattern(args); |
504 | this.error_in_other_files.push(pat); |
505 | } |
506 | "stderr-per-bitwidth" => (this, _args, span){ |
507 | // args are ignored (can be used as comment) |
508 | this.check( |
509 | span, |
510 | !this.stderr_per_bitwidth, |
511 | "cannot specify `stderr-per-bitwidth` twice" , |
512 | ); |
513 | this.stderr_per_bitwidth = true; |
514 | } |
515 | "run-rustfix" => (this, _args, span){ |
516 | this.error(span, "rustfix is now ran by default when applicable suggestions are found" ); |
517 | } |
518 | "no-rustfix" => (this, _args, span){ |
519 | // args are ignored (can be used as comment) |
520 | let prev = this.no_rustfix.set((), span); |
521 | this.check( |
522 | span, |
523 | prev.is_none(), |
524 | "cannot specify `no-rustfix` twice" , |
525 | ); |
526 | } |
527 | "needs-asm-support" => (this, _args, span){ |
528 | // args are ignored (can be used as comment) |
529 | this.check( |
530 | span, |
531 | !this.needs_asm_support, |
532 | "cannot specify `needs-asm-support` twice" , |
533 | ); |
534 | this.needs_asm_support = true; |
535 | } |
536 | "aux-build" => (this, args, _span){ |
537 | let name = match args.split_once(":" ) { |
538 | Some((name, rest)) => { |
539 | this.error(rest.span(), "proc macros are now auto-detected, you can remove the `:proc-macro` after the file name" ); |
540 | name |
541 | }, |
542 | None => args, |
543 | }; |
544 | this.aux_builds.push(name.map(Into::into)); |
545 | } |
546 | "edition" => (this, args, span){ |
547 | let prev = this.edition.set((*args).into(), args.span()); |
548 | this.check(span, prev.is_none(), "cannot specify `edition` twice" ); |
549 | } |
550 | "check-pass" => (this, _args, span){ |
551 | let prev = this.mode.set(Mode::Pass, span); |
552 | // args are ignored (can be used as comment) |
553 | this.check( |
554 | span, |
555 | prev.is_none(), |
556 | "cannot specify test mode changes twice" , |
557 | ); |
558 | } |
559 | "run" => (this, args, span){ |
560 | this.check( |
561 | span, |
562 | this.mode.is_none(), |
563 | "cannot specify test mode changes twice" , |
564 | ); |
565 | let mut set = |exit_code| this.mode.set(Mode::Run { exit_code }, args.span()); |
566 | if args.is_empty() { |
567 | set(0); |
568 | } else { |
569 | match args.parse() { |
570 | Ok(exit_code) => {set(exit_code);}, |
571 | Err(err) => this.error(args.span(), err.to_string()), |
572 | } |
573 | } |
574 | } |
575 | "require-annotations-for-level" => (this, args, span){ |
576 | let args = args.trim(); |
577 | let prev = match args.parse() { |
578 | Ok(it) => this.require_annotations_for_level.set(it, args.span()), |
579 | Err(msg) => { |
580 | this.error(args.span(), msg); |
581 | None |
582 | }, |
583 | }; |
584 | |
585 | this.check( |
586 | span, |
587 | prev.is_none(), |
588 | "cannot specify `require-annotations-for-level` twice" , |
589 | ); |
590 | } |
591 | } |
592 | commands |
593 | } |
594 | |
595 | fn parse_command(&mut self, command: Spanned<&str>, args: Spanned<&str>) { |
596 | if let Some(command_handler) = self.commands.get(*command) { |
597 | command_handler(self, args, command.span()); |
598 | } else if let Some(s) = command.strip_prefix("ignore-" ) { |
599 | // args are ignored (can be used as comment) |
600 | match Condition::parse(*s) { |
601 | Ok(cond) => self.ignore.push(cond), |
602 | Err(msg) => self.error(s.span(), msg), |
603 | } |
604 | } else if let Some(s) = command.strip_prefix("only-" ) { |
605 | // args are ignored (can be used as comment) |
606 | match Condition::parse(*s) { |
607 | Ok(cond) => self.only.push(cond), |
608 | Err(msg) => self.error(s.span(), msg), |
609 | } |
610 | } else { |
611 | let best_match = self |
612 | .commands |
613 | .keys() |
614 | .min_by_key(|key| levenshtein::levenshtein(key, *command)) |
615 | .unwrap(); |
616 | self.error( |
617 | command.span(), |
618 | format!( |
619 | "` {}` is not a command known to `ui_test`, did you mean ` {best_match}`?" , |
620 | *command |
621 | ), |
622 | ); |
623 | } |
624 | } |
625 | } |
626 | |
627 | impl<CommentsType> CommentParser<CommentsType> { |
628 | fn parse_regex(&mut self, regex: Spanned<&str>) -> Option<Regex> { |
629 | match Regex::new(*regex) { |
630 | Ok(regex) => Some(regex), |
631 | Err(err) => { |
632 | self.error(regex.span(), format!("invalid regex: {err:?}" )); |
633 | None |
634 | } |
635 | } |
636 | } |
637 | |
638 | /// Parses a string literal. `s` has to start with `"`; everything until the next `"` is |
639 | /// returned in the first component. `\` can be used to escape arbitrary character. |
640 | /// Second return component is the rest of the string with leading whitespace removed. |
641 | fn parse_str<'a>(&mut self, s: Spanned<&'a str>) -> (Spanned<&'a str>, Spanned<&'a str>) { |
642 | match s.strip_prefix(" \"" ) { |
643 | Some(s) => { |
644 | let mut escaped = false; |
645 | for (i, c) in s.char_indices() { |
646 | if escaped { |
647 | // Accept any character as literal after a `\`. |
648 | escaped = false; |
649 | } else if c == '"' { |
650 | let (a, b) = s.split_at(i); |
651 | let b = b.split_at(1).1; |
652 | return (a, b.trim_start()); |
653 | } else { |
654 | escaped = c == ' \\' ; |
655 | } |
656 | } |
657 | self.error(s.span(), format!("no closing quotes found for {}" , *s)); |
658 | (s, Spanned::new("" , s.span())) |
659 | } |
660 | None => { |
661 | if s.is_empty() { |
662 | self.error(s.span(), "expected quoted string, but found end of line" ) |
663 | } else { |
664 | self.error( |
665 | s.span(), |
666 | format!("expected ` \"`, got ` {}`" , s.chars().next().unwrap()), |
667 | ) |
668 | } |
669 | (s, Spanned::new("" , s.span())) |
670 | } |
671 | } |
672 | } |
673 | |
674 | // parse something like \[[a-z]+(,[a-z]+)*\] |
675 | fn parse_revisions<'a>( |
676 | &mut self, |
677 | pattern: Spanned<&'a str>, |
678 | ) -> (Spanned<Vec<String>>, Spanned<&'a str>) { |
679 | match pattern.strip_prefix("[" ) { |
680 | Some(s) => { |
681 | // revisions |
682 | let end = s.char_indices().find_map(|(i, c)| match c { |
683 | ']' => Some(i), |
684 | _ => None, |
685 | }); |
686 | let Some(end) = end else { |
687 | self.error(s.span(), "`[` without corresponding `]`" ); |
688 | return ( |
689 | Spanned::new(vec![], pattern.span().shrink_to_start()), |
690 | pattern, |
691 | ); |
692 | }; |
693 | let (revision, pattern) = s.split_at(end); |
694 | let revisions = revision.split(',' ).map(|s| s.trim().to_string()).collect(); |
695 | ( |
696 | Spanned::new(revisions, revision.span()), |
697 | // 1.. because `split_at` includes the separator |
698 | pattern.split_at(1).1.trim_start(), |
699 | ) |
700 | } |
701 | _ => ( |
702 | Spanned::new(vec![], pattern.span().shrink_to_start()), |
703 | pattern, |
704 | ), |
705 | } |
706 | } |
707 | } |
708 | |
709 | impl CommentParser<&mut Revisioned> { |
710 | // parse something like (\[[a-z]+(,[a-z]+)*\])?(?P<offset>\||[\^]+)? *(?P<level>ERROR|HELP|WARN|NOTE): (?P<text>.*) |
711 | fn parse_pattern(&mut self, pattern: Spanned<&str>, fallthrough_to: &mut Option<NonZeroUsize>) { |
712 | let (match_line, pattern) = match pattern.chars().next() { |
713 | Some('|' ) => ( |
714 | match fallthrough_to { |
715 | Some(fallthrough) => *fallthrough, |
716 | None => { |
717 | self.error(pattern.span(), "`//~|` pattern without preceding line" ); |
718 | return; |
719 | } |
720 | }, |
721 | pattern.split_at(1).1, |
722 | ), |
723 | Some('^' ) => { |
724 | let offset = pattern.chars().take_while(|&c| c == '^' ).count(); |
725 | match pattern |
726 | .span() |
727 | .line_start |
728 | .get() |
729 | .checked_sub(offset) |
730 | .and_then(NonZeroUsize::new) |
731 | { |
732 | // lines are one-indexed, so a target line of 0 is invalid, but also |
733 | // prevented via `NonZeroUsize` |
734 | Some(match_line) => (match_line, pattern.split_at(offset).1), |
735 | _ => { |
736 | self.error(pattern.span(), format!( |
737 | "//~^ pattern is trying to refer to {} lines above, but there are only {} lines above" , |
738 | offset, |
739 | pattern.line().get() - 1, |
740 | )); |
741 | return; |
742 | } |
743 | } |
744 | } |
745 | Some(_) => (pattern.span().line_start, pattern), |
746 | None => { |
747 | self.error(pattern.span(), "no pattern specified" ); |
748 | return; |
749 | } |
750 | }; |
751 | |
752 | let pattern = pattern.trim_start(); |
753 | let offset = match pattern.chars().position(|c| !c.is_ascii_alphabetic()) { |
754 | Some(offset) => offset, |
755 | None => { |
756 | self.error(pattern.span(), "pattern without level" ); |
757 | return; |
758 | } |
759 | }; |
760 | |
761 | let (level, pattern) = pattern.split_at(offset); |
762 | let level = match (*level).parse() { |
763 | Ok(level) => level, |
764 | Err(msg) => { |
765 | self.error(level.span(), msg); |
766 | return; |
767 | } |
768 | }; |
769 | let pattern = match pattern.strip_prefix(":" ) { |
770 | Some(offset) => offset, |
771 | None => { |
772 | self.error(pattern.span(), "no `:` after level found" ); |
773 | return; |
774 | } |
775 | }; |
776 | |
777 | let pattern = pattern.trim(); |
778 | |
779 | self.check(pattern.span(), !pattern.is_empty(), "no pattern specified" ); |
780 | |
781 | let pattern = self.parse_error_pattern(pattern); |
782 | |
783 | *fallthrough_to = Some(match_line); |
784 | |
785 | self.error_matches.push(ErrorMatch { |
786 | pattern, |
787 | level, |
788 | line: match_line, |
789 | }); |
790 | } |
791 | } |
792 | |
793 | impl Pattern { |
794 | pub(crate) fn matches(&self, message: &str) -> bool { |
795 | match self { |
796 | Pattern::SubString(s: &String) => message.contains(s), |
797 | Pattern::Regex(r: &Regex) => r.is_match(text:message.as_bytes()), |
798 | } |
799 | } |
800 | } |
801 | |
802 | impl<CommentsType> CommentParser<CommentsType> { |
803 | fn parse_error_pattern(&mut self, pattern: Spanned<&str>) -> Spanned<Pattern> { |
804 | if let Some(regex: Spanned<&str>) = pattern.strip_prefix("/" ) { |
805 | match regex.strip_suffix("/" ) { |
806 | Some(regex: Spanned<&str>) => match self.parse_regex(regex) { |
807 | Some(r: Regex) => Spanned::new(data:Pattern::Regex(r), regex.span()), |
808 | None => Spanned::new(data:Pattern::SubString(pattern.to_string()), regex.span()), |
809 | }, |
810 | None => { |
811 | self.error( |
812 | regex.span(), |
813 | s:"expected regex pattern due to leading `/`, but found no closing `/`" , |
814 | ); |
815 | Spanned::new(data:Pattern::SubString(pattern.to_string()), regex.span()) |
816 | } |
817 | } |
818 | } else { |
819 | Spanned::new(data:Pattern::SubString(pattern.to_string()), pattern.span()) |
820 | } |
821 | } |
822 | } |
823 | |