| 1 | use crate::{ | 
| 2 | custom_flags::Flag, diagnostics::Level, filter::Match, test_result::Errored, Config, Error, | 
|---|
| 3 | }; | 
|---|
| 4 | use bstr::{ByteSlice, Utf8Error}; | 
|---|
| 5 | use color_eyre::eyre::Result; | 
|---|
| 6 | use regex::bytes::Regex; | 
|---|
| 7 | pub use spanned::*; | 
|---|
| 8 | use std::{ | 
|---|
| 9 | collections::{BTreeMap, HashMap}, | 
|---|
| 10 | num::NonZeroUsize, | 
|---|
| 11 | }; | 
|---|
| 12 |  | 
|---|
| 13 | mod spanned; | 
|---|
| 14 | #[ cfg(test)] | 
|---|
| 15 | mod tests; | 
|---|
| 16 |  | 
|---|
| 17 | /// This crate supports various magic comments that get parsed as file-specific | 
|---|
| 18 | /// configuration values. This struct parses them all in one go and then they | 
|---|
| 19 | /// get processed by their respective use sites. | 
|---|
| 20 | #[ derive(Debug, Clone)] | 
|---|
| 21 | pub struct Comments { | 
|---|
| 22 | /// List of revision names to execute. Can only be specified once | 
|---|
| 23 | pub revisions: Option<Vec<String>>, | 
|---|
| 24 | /// Comments that are only available under specific revisions. | 
|---|
| 25 | /// The defaults are in key `vec![]` | 
|---|
| 26 | pub revisioned: BTreeMap<Vec<String>, Revisioned>, | 
|---|
| 27 | } | 
|---|
| 28 |  | 
|---|
| 29 | impl Default for Comments { | 
|---|
| 30 | fn default() -> Self { | 
|---|
| 31 | let mut this: Comments = Self { | 
|---|
| 32 | revisions: Default::default(), | 
|---|
| 33 | revisioned: Default::default(), | 
|---|
| 34 | }; | 
|---|
| 35 | this.revisioned.insert(key:vec![], value:Revisioned::default()); | 
|---|
| 36 | this | 
|---|
| 37 | } | 
|---|
| 38 | } | 
|---|
| 39 |  | 
|---|
| 40 | impl Comments { | 
|---|
| 41 | /// Check that a comment isn't specified twice across multiple differently revisioned statements. | 
|---|
| 42 | /// e.g. `//@[foo, bar] error-in-other-file: bop` and `//@[foo, baz] error-in-other-file boop` would end up | 
|---|
| 43 | /// specifying two error patterns that are available in revision `foo`. | 
|---|
| 44 | pub fn find_one_for_revision<'a, T: 'a>( | 
|---|
| 45 | &'a self, | 
|---|
| 46 | revision: &'a str, | 
|---|
| 47 | kind: &str, | 
|---|
| 48 | f: impl Fn(&'a Revisioned) -> OptWithLine<T>, | 
|---|
| 49 | ) -> Result<OptWithLine<T>, Errored> { | 
|---|
| 50 | let mut result = None; | 
|---|
| 51 | let mut errors = vec![]; | 
|---|
| 52 | for (k, rev) in &self.revisioned { | 
|---|
| 53 | if !k.iter().any(|r| r == revision) { | 
|---|
| 54 | continue; | 
|---|
| 55 | } | 
|---|
| 56 | if let Some(found) = f(rev).into_inner() { | 
|---|
| 57 | if result.is_some() { | 
|---|
| 58 | errors.push(found.span); | 
|---|
| 59 | } else { | 
|---|
| 60 | result = found.into(); | 
|---|
| 61 | } | 
|---|
| 62 | } | 
|---|
| 63 | } | 
|---|
| 64 | if result.is_none() { | 
|---|
| 65 | result = f(&self.revisioned[&[][..]]).into_inner(); | 
|---|
| 66 | } | 
|---|
| 67 | if errors.is_empty() { | 
|---|
| 68 | Ok(result.into()) | 
|---|
| 69 | } else { | 
|---|
| 70 | Err(Errored { | 
|---|
| 71 | command: format!( "<finding flags for revision `{revision} `>"), | 
|---|
| 72 | errors: vec![Error::MultipleRevisionsWithResults { | 
|---|
| 73 | kind: kind.to_string(), | 
|---|
| 74 | lines: errors, | 
|---|
| 75 | }], | 
|---|
| 76 | stderr: vec![], | 
|---|
| 77 | stdout: vec![], | 
|---|
| 78 | }) | 
|---|
| 79 | } | 
|---|
| 80 | } | 
|---|
| 81 |  | 
|---|
| 82 | /// Returns an iterator over all revisioned comments that match the revision. | 
|---|
| 83 | pub fn for_revision<'a>(&'a self, revision: &'a str) -> impl Iterator<Item = &'a Revisioned> { | 
|---|
| 84 | [&self.revisioned[&[][..]]].into_iter().chain( | 
|---|
| 85 | self.revisioned | 
|---|
| 86 | .iter() | 
|---|
| 87 | .filter_map(move |(k, v)| k.iter().any(|rev| rev == revision).then_some(v)), | 
|---|
| 88 | ) | 
|---|
| 89 | } | 
|---|
| 90 |  | 
|---|
| 91 | /// The comments set for all revisions | 
|---|
| 92 | pub fn base(&mut self) -> &mut Revisioned { | 
|---|
| 93 | self.revisioned.get_mut(&[][..]).unwrap() | 
|---|
| 94 | } | 
|---|
| 95 |  | 
|---|
| 96 | /// The comments set for all revisions | 
|---|
| 97 | pub fn base_immut(&self) -> &Revisioned { | 
|---|
| 98 | self.revisioned.get(&[][..]).unwrap() | 
|---|
| 99 | } | 
|---|
| 100 |  | 
|---|
| 101 | pub(crate) fn exit_status(&self, revision: &str) -> Result<Option<Spanned<i32>>, Errored> { | 
|---|
| 102 | Ok(self | 
|---|
| 103 | .find_one_for_revision(revision, "`exit_status` annotations", |r| { | 
|---|
| 104 | r.exit_status.clone() | 
|---|
| 105 | })? | 
|---|
| 106 | .into_inner()) | 
|---|
| 107 | } | 
|---|
| 108 |  | 
|---|
| 109 | pub(crate) fn require_annotations(&self, revision: &str) -> Option<Spanned<bool>> { | 
|---|
| 110 | self.for_revision(revision).fold(None, |acc, elem| { | 
|---|
| 111 | elem.require_annotations.as_ref().cloned().or(acc) | 
|---|
| 112 | }) | 
|---|
| 113 | } | 
|---|
| 114 | } | 
|---|
| 115 |  | 
|---|
| 116 | #[ derive(Debug, Clone, Default)] | 
|---|
| 117 | /// Comments that can be filtered for specific revisions. | 
|---|
| 118 | pub struct Revisioned { | 
|---|
| 119 | /// The character range in which this revisioned item was first added. | 
|---|
| 120 | /// Used for reporting errors on unknown revisions. | 
|---|
| 121 | pub span: Span, | 
|---|
| 122 | /// Don't run this test if any of these filters apply | 
|---|
| 123 | pub ignore: Vec<Condition>, | 
|---|
| 124 | /// Only run this test if all of these filters apply | 
|---|
| 125 | pub only: Vec<Condition>, | 
|---|
| 126 | /// Generate one .stderr file per bit width, by prepending with `.64bit` and similar | 
|---|
| 127 | pub stderr_per_bitwidth: bool, | 
|---|
| 128 | /// Additional flags to pass to the executable | 
|---|
| 129 | pub compile_flags: Vec<String>, | 
|---|
| 130 | /// Additional env vars to set for the executable | 
|---|
| 131 | pub env_vars: Vec<(String, String)>, | 
|---|
| 132 | /// Normalizations to apply to the stderr output before emitting it to disk | 
|---|
| 133 | pub normalize_stderr: Vec<(Match, Vec<u8>)>, | 
|---|
| 134 | /// Normalizations to apply to the stdout output before emitting it to disk | 
|---|
| 135 | pub normalize_stdout: Vec<(Match, Vec<u8>)>, | 
|---|
| 136 | /// Arbitrary patterns to look for in the stderr. | 
|---|
| 137 | /// The error must be from another file, as errors from the current file must be | 
|---|
| 138 | /// checked via `error_matches`. | 
|---|
| 139 | pub(crate) error_in_other_files: Vec<Spanned<Pattern>>, | 
|---|
| 140 | pub(crate) error_matches: Vec<ErrorMatch>, | 
|---|
| 141 | /// Ignore diagnostics below this level. | 
|---|
| 142 | /// `None` means pick the lowest level from the `error_pattern`s. | 
|---|
| 143 | pub require_annotations_for_level: OptWithLine<Level>, | 
|---|
| 144 | /// The exit status that the driver is expected to emit. | 
|---|
| 145 | /// If `None`, any exit status is accepted. | 
|---|
| 146 | pub exit_status: OptWithLine<i32>, | 
|---|
| 147 | /// `Some(true)` means annotations are required | 
|---|
| 148 | /// `Some(false)` means annotations are forbidden | 
|---|
| 149 | /// `None` means this revision does not change the base annoatation requirement. | 
|---|
| 150 | pub require_annotations: OptWithLine<bool>, | 
|---|
| 151 | /// Prefix added to all diagnostic code matchers. Note this will make it impossible | 
|---|
| 152 | /// match codes which do not contain this prefix. | 
|---|
| 153 | pub diagnostic_code_prefix: OptWithLine<String>, | 
|---|
| 154 | /// Tester-specific flags. | 
|---|
| 155 | /// The keys are just labels for overwriting or retrieving the value later. | 
|---|
| 156 | /// They are mostly used by `Config::custom_comments` handlers, | 
|---|
| 157 | /// `ui_test` itself only ever looks at the values, not the keys. | 
|---|
| 158 | /// | 
|---|
| 159 | /// You usually don't modify this directly but use the `add_custom` or `set_custom_once` | 
|---|
| 160 | /// helpers. | 
|---|
| 161 | pub custom: BTreeMap<&'static str, Spanned<Vec<Box<dyn Flag>>>>, | 
|---|
| 162 | } | 
|---|
| 163 |  | 
|---|
| 164 | impl Revisioned { | 
|---|
| 165 | /// Append another flag to an existing or new key | 
|---|
| 166 | pub fn add_custom(&mut self, key: &'static str, custom: impl Flag + 'static) { | 
|---|
| 167 | self.add_custom_spanned(key, custom, Span::default()) | 
|---|
| 168 | } | 
|---|
| 169 |  | 
|---|
| 170 | /// Append another flag to an existing or new key | 
|---|
| 171 | pub fn add_custom_spanned( | 
|---|
| 172 | &mut self, | 
|---|
| 173 | key: &'static str, | 
|---|
| 174 | custom: impl Flag + 'static, | 
|---|
| 175 | span: Span, | 
|---|
| 176 | ) { | 
|---|
| 177 | self.custom | 
|---|
| 178 | .entry(key) | 
|---|
| 179 | .or_insert_with(|| Spanned::new(vec![], span)) | 
|---|
| 180 | .content | 
|---|
| 181 | .push(Box::new(custom)); | 
|---|
| 182 | } | 
|---|
| 183 | /// Override or set a flag | 
|---|
| 184 | pub fn set_custom(&mut self, key: &'static str, custom: impl Flag + 'static) { | 
|---|
| 185 | self.custom | 
|---|
| 186 | .insert(key, Spanned::dummy(vec![Box::new(custom)])); | 
|---|
| 187 | } | 
|---|
| 188 | } | 
|---|
| 189 |  | 
|---|
| 190 | /// Main entry point to parsing comments and handling parsing errors. | 
|---|
| 191 | #[ derive(Debug)] | 
|---|
| 192 | pub struct CommentParser<T> { | 
|---|
| 193 | /// The comments being built. | 
|---|
| 194 | comments: T, | 
|---|
| 195 | /// Any errors that ocurred during comment parsing. | 
|---|
| 196 | errors: Vec<Error>, | 
|---|
| 197 | /// The available commands and their parsing logic | 
|---|
| 198 | commands: HashMap<&'static str, CommandParserFunc>, | 
|---|
| 199 | /// The symbol(s) that signify the start of a comment. | 
|---|
| 200 | comment_start: &'static str, | 
|---|
| 201 | } | 
|---|
| 202 |  | 
|---|
| 203 | /// Command parser function type. | 
|---|
| 204 | pub type CommandParserFunc = | 
|---|
| 205 | fn(&mut CommentParser<&mut Revisioned>, args: Spanned<&str>, span: Span); | 
|---|
| 206 |  | 
|---|
| 207 | impl<T> std::ops::Deref for CommentParser<T> { | 
|---|
| 208 | type Target = T; | 
|---|
| 209 |  | 
|---|
| 210 | fn deref(&self) -> &Self::Target { | 
|---|
| 211 | &self.comments | 
|---|
| 212 | } | 
|---|
| 213 | } | 
|---|
| 214 |  | 
|---|
| 215 | impl<T> std::ops::DerefMut for CommentParser<T> { | 
|---|
| 216 | fn deref_mut(&mut self) -> &mut Self::Target { | 
|---|
| 217 | &mut self.comments | 
|---|
| 218 | } | 
|---|
| 219 | } | 
|---|
| 220 |  | 
|---|
| 221 | /// The conditions used for "ignore" and "only" filters. | 
|---|
| 222 | #[ derive(Debug, Clone)] | 
|---|
| 223 | pub enum Condition { | 
|---|
| 224 | /// One of the given strings must appear in the host triple. | 
|---|
| 225 | Host(Vec<TargetSubStr>), | 
|---|
| 226 | /// One of the given string must appear in the target triple. | 
|---|
| 227 | Target(Vec<TargetSubStr>), | 
|---|
| 228 | /// Tests that the bitwidth is one of the given ones. | 
|---|
| 229 | Bitwidth(Vec<u8>), | 
|---|
| 230 | /// Tests that the target is the host. | 
|---|
| 231 | OnHost, | 
|---|
| 232 | } | 
|---|
| 233 |  | 
|---|
| 234 | /// A sub string of a target (or a whole target). | 
|---|
| 235 | /// | 
|---|
| 236 | /// Effectively a `String` that only allows lowercase chars, integers and dashes. | 
|---|
| 237 | #[ derive(Debug, Clone)] | 
|---|
| 238 | pub struct TargetSubStr(String); | 
|---|
| 239 |  | 
|---|
| 240 | impl PartialEq<&str> for TargetSubStr { | 
|---|
| 241 | fn eq(&self, other: &&str) -> bool { | 
|---|
| 242 | self.0 == *other | 
|---|
| 243 | } | 
|---|
| 244 | } | 
|---|
| 245 |  | 
|---|
| 246 | impl std::ops::Deref for TargetSubStr { | 
|---|
| 247 | type Target = str; | 
|---|
| 248 |  | 
|---|
| 249 | fn deref(&self) -> &Self::Target { | 
|---|
| 250 | &self.0 | 
|---|
| 251 | } | 
|---|
| 252 | } | 
|---|
| 253 |  | 
|---|
| 254 | impl TryFrom<String> for TargetSubStr { | 
|---|
| 255 | type Error = String; | 
|---|
| 256 |  | 
|---|
| 257 | fn try_from(value: String) -> std::result::Result<Self, Self::Error> { | 
|---|
| 258 | if valueChars<'_> | 
|---|
| 259 | .chars() | 
|---|
| 260 | .all(|c: char| c.is_ascii_alphanumeric() || c == '-'|| c == '_') | 
|---|
| 261 | { | 
|---|
| 262 | Ok(Self(value)) | 
|---|
| 263 | } else { | 
|---|
| 264 | Err(format!( | 
|---|
| 265 | "target strings can only contain integers, basic alphabet characters or dashes" | 
|---|
| 266 | )) | 
|---|
| 267 | } | 
|---|
| 268 | } | 
|---|
| 269 | } | 
|---|
| 270 |  | 
|---|
| 271 | /// An error pattern parsed from a `//~` comment. | 
|---|
| 272 | #[ derive(Debug, Clone)] | 
|---|
| 273 | pub enum Pattern { | 
|---|
| 274 | /// A substring that must appear in the error message. | 
|---|
| 275 | SubString(String), | 
|---|
| 276 | /// A regex that must match the error message. | 
|---|
| 277 | Regex(Regex), | 
|---|
| 278 | } | 
|---|
| 279 |  | 
|---|
| 280 | #[ derive(Debug, Clone)] | 
|---|
| 281 | pub(crate) enum ErrorMatchKind { | 
|---|
| 282 | /// A level and pattern pair parsed from a `//~ LEVEL: Message` comment. | 
|---|
| 283 | Pattern { | 
|---|
| 284 | pattern: Spanned<Pattern>, | 
|---|
| 285 | level: Level, | 
|---|
| 286 | }, | 
|---|
| 287 | /// An error code parsed from a `//~ error_code` comment. | 
|---|
| 288 | Code(Spanned<String>), | 
|---|
| 289 | } | 
|---|
| 290 |  | 
|---|
| 291 | #[ derive(Debug, Clone)] | 
|---|
| 292 | pub(crate) struct ErrorMatch { | 
|---|
| 293 | pub(crate) kind: ErrorMatchKind, | 
|---|
| 294 | /// The line this pattern is expecting to find a message in. | 
|---|
| 295 | pub(crate) line: NonZeroUsize, | 
|---|
| 296 | } | 
|---|
| 297 |  | 
|---|
| 298 | impl Condition { | 
|---|
| 299 | fn parse(c: &str, args: &str) -> std::result::Result<Self, String> { | 
|---|
| 300 | let args: SplitWhitespace<'_> = args.split_whitespace(); | 
|---|
| 301 | match c { | 
|---|
| 302 | "on-host"=> Ok(Condition::OnHost), | 
|---|
| 303 | "bitwidth"=> { | 
|---|
| 304 | let bits: Vec = args.map(|arg: &str| arg.parse::<u8>().map_err(|_err: ParseIntError| { | 
|---|
| 305 | format!( "invalid ignore/only filter ending in 'bit': {c:?}  is not a valid bitwdith") | 
|---|
| 306 | })).collect::<Result<Vec<_>, _>>()?; | 
|---|
| 307 | Ok(Condition::Bitwidth(bits)) | 
|---|
| 308 | } | 
|---|
| 309 | "target"=> Ok(Condition::Target(args.take_while(|&arg: &str| arg != "#").map(|arg: &str|TargetSubStr::try_from(arg.to_owned())).collect::<Result<_, _>>()?)), | 
|---|
| 310 | "host"=> Ok(Condition::Host(args.take_while(|&arg: &str| arg != "#").map(|arg: &str|TargetSubStr::try_from(arg.to_owned())).collect::<Result<_, _>>()?)), | 
|---|
| 311 | _ => Err(format!( "`{c} ` is not a valid condition, expected `on-host`, /[0-9]+bit/, /host-.*/, or /target-.*/")), | 
|---|
| 312 | } | 
|---|
| 313 | } | 
|---|
| 314 | } | 
|---|
| 315 |  | 
|---|
| 316 | enum ParsePatternResult { | 
|---|
| 317 | Other, | 
|---|
| 318 | ErrorAbove { | 
|---|
| 319 | match_line: NonZeroUsize, | 
|---|
| 320 | }, | 
|---|
| 321 | ErrorBelow { | 
|---|
| 322 | span: Span, | 
|---|
| 323 | match_line: NonZeroUsize, | 
|---|
| 324 | }, | 
|---|
| 325 | Fallthrough { | 
|---|
| 326 | span: Span, | 
|---|
| 327 | idx: usize, | 
|---|
| 328 | }, | 
|---|
| 329 | } | 
|---|
| 330 |  | 
|---|
| 331 | impl Comments { | 
|---|
| 332 | /// Parse comments in `content`. | 
|---|
| 333 | /// `path` is only used to emit diagnostics if parsing fails. | 
|---|
| 334 | pub(crate) fn parse( | 
|---|
| 335 | content: Spanned<&[u8]>, | 
|---|
| 336 | config: &Config, | 
|---|
| 337 | ) -> std::result::Result<Self, Vec<Error>> { | 
|---|
| 338 | CommentParser::new(config).parse(content) | 
|---|
| 339 | } | 
|---|
| 340 | } | 
|---|
| 341 |  | 
|---|
| 342 | impl CommentParser<Comments> { | 
|---|
| 343 | fn new(config: &Config) -> Self { | 
|---|
| 344 | let mut this = Self { | 
|---|
| 345 | comments: config.comment_defaults.clone(), | 
|---|
| 346 | errors: vec![], | 
|---|
| 347 | commands: Self::commands(), | 
|---|
| 348 | comment_start: config.comment_start, | 
|---|
| 349 | }; | 
|---|
| 350 | this.commands | 
|---|
| 351 | .extend(config.custom_comments.iter().map(|(&k, &v)| (k, v))); | 
|---|
| 352 | this | 
|---|
| 353 | } | 
|---|
| 354 |  | 
|---|
| 355 | fn parse(mut self, content: Spanned<&[u8]>) -> std::result::Result<Comments, Vec<Error>> { | 
|---|
| 356 | // We take out the existing flags so that we can ensure every test only sets them once | 
|---|
| 357 | // by checking that they haven't already been set. | 
|---|
| 358 | let mut defaults = std::mem::take(self.comments.revisioned.get_mut(&[][..]).unwrap()); | 
|---|
| 359 |  | 
|---|
| 360 | let mut delayed_fallthrough = Vec::new(); | 
|---|
| 361 | let mut fallthrough_to = None; // The line that a `|` will refer to. | 
|---|
| 362 | let mut last_line = 0; | 
|---|
| 363 | for (l, line) in content.lines().enumerate() { | 
|---|
| 364 | last_line = l + 1; | 
|---|
| 365 | let l = NonZeroUsize::new(l + 1).unwrap(); // enumerate starts at 0, but line numbers start at 1 | 
|---|
| 366 | match self.parse_checked_line(fallthrough_to, l, line) { | 
|---|
| 367 | Ok(ParsePatternResult::Other) => { | 
|---|
| 368 | fallthrough_to = None; | 
|---|
| 369 | } | 
|---|
| 370 | Ok(ParsePatternResult::ErrorAbove { match_line }) => { | 
|---|
| 371 | fallthrough_to = Some(match_line); | 
|---|
| 372 | } | 
|---|
| 373 | Ok(ParsePatternResult::Fallthrough { span, idx }) => { | 
|---|
| 374 | delayed_fallthrough.push((span, l, idx)); | 
|---|
| 375 | } | 
|---|
| 376 | Ok(ParsePatternResult::ErrorBelow { span, match_line }) => { | 
|---|
| 377 | if fallthrough_to.is_some() { | 
|---|
| 378 | self.error( | 
|---|
| 379 | span, | 
|---|
| 380 | "`//~v` comment immediately following a `//~^` comment chain", | 
|---|
| 381 | ); | 
|---|
| 382 | } | 
|---|
| 383 |  | 
|---|
| 384 | for (span, line, idx) in delayed_fallthrough.drain(..) { | 
|---|
| 385 | if let Some(rev) = self | 
|---|
| 386 | .comments | 
|---|
| 387 | .revisioned | 
|---|
| 388 | .values_mut() | 
|---|
| 389 | .find(|rev| rev.error_matches[idx].line == line) | 
|---|
| 390 | { | 
|---|
| 391 | rev.error_matches[idx].line = match_line; | 
|---|
| 392 | } else { | 
|---|
| 393 | self.error(span, "`//~|` comment not attached to anchoring matcher"); | 
|---|
| 394 | } | 
|---|
| 395 | } | 
|---|
| 396 | } | 
|---|
| 397 | Err(e) => self.error(e.span, format!( "Comment is not utf8: {:?} ", e.content)), | 
|---|
| 398 | } | 
|---|
| 399 | } | 
|---|
| 400 | if let Some(revisions) = &self.comments.revisions { | 
|---|
| 401 | for (key, revisioned) in &self.comments.revisioned { | 
|---|
| 402 | for rev in key { | 
|---|
| 403 | if !revisions.contains(rev) { | 
|---|
| 404 | self.errors.push(Error::InvalidComment { | 
|---|
| 405 | msg: format!( "the revision `{rev} ` is not known"), | 
|---|
| 406 | span: revisioned.span.clone(), | 
|---|
| 407 | }) | 
|---|
| 408 | } | 
|---|
| 409 | } | 
|---|
| 410 | } | 
|---|
| 411 | } else { | 
|---|
| 412 | for (key, revisioned) in &self.comments.revisioned { | 
|---|
| 413 | if !key.is_empty() { | 
|---|
| 414 | self.errors.push(Error::InvalidComment { | 
|---|
| 415 | msg: "there are no revisions in this test".into(), | 
|---|
| 416 | span: revisioned.span.clone(), | 
|---|
| 417 | }) | 
|---|
| 418 | } | 
|---|
| 419 | } | 
|---|
| 420 | } | 
|---|
| 421 |  | 
|---|
| 422 | for revisioned in self.comments.revisioned.values() { | 
|---|
| 423 | for m in &revisioned.error_matches { | 
|---|
| 424 | if m.line.get() > last_line { | 
|---|
| 425 | let span = match &m.kind { | 
|---|
| 426 | ErrorMatchKind::Pattern { pattern, .. } => pattern.span(), | 
|---|
| 427 | ErrorMatchKind::Code(code) => code.span(), | 
|---|
| 428 | }; | 
|---|
| 429 | self.errors.push(Error::InvalidComment { | 
|---|
| 430 | msg: format!( | 
|---|
| 431 | "//~v pattern is trying to refer to line {} , but the file only has {}  lines", | 
|---|
| 432 | m.line.get(), | 
|---|
| 433 | last_line, | 
|---|
| 434 | ), | 
|---|
| 435 | span, | 
|---|
| 436 | }); | 
|---|
| 437 | } | 
|---|
| 438 | } | 
|---|
| 439 | } | 
|---|
| 440 |  | 
|---|
| 441 | for (span, ..) in delayed_fallthrough { | 
|---|
| 442 | self.error(span, "`//~|` comment not attached to anchoring matcher"); | 
|---|
| 443 | } | 
|---|
| 444 |  | 
|---|
| 445 | let Revisioned { | 
|---|
| 446 | span, | 
|---|
| 447 | ignore, | 
|---|
| 448 | only, | 
|---|
| 449 | stderr_per_bitwidth, | 
|---|
| 450 | compile_flags, | 
|---|
| 451 | env_vars, | 
|---|
| 452 | normalize_stderr, | 
|---|
| 453 | normalize_stdout, | 
|---|
| 454 | error_in_other_files, | 
|---|
| 455 | error_matches, | 
|---|
| 456 | require_annotations_for_level, | 
|---|
| 457 | exit_status, | 
|---|
| 458 | require_annotations, | 
|---|
| 459 | diagnostic_code_prefix, | 
|---|
| 460 | custom, | 
|---|
| 461 | } = &mut defaults; | 
|---|
| 462 |  | 
|---|
| 463 | // We insert into the defaults so that the defaults are first in case of sorted lists | 
|---|
| 464 | // like `normalize_stderr`, `compile_flags`, or `env_vars` | 
|---|
| 465 | let base = std::mem::take(self.comments.base()); | 
|---|
| 466 | if span.is_dummy() { | 
|---|
| 467 | *span = base.span; | 
|---|
| 468 | } | 
|---|
| 469 | ignore.extend(base.ignore); | 
|---|
| 470 | only.extend(base.only); | 
|---|
| 471 | *stderr_per_bitwidth |= base.stderr_per_bitwidth; | 
|---|
| 472 | compile_flags.extend(base.compile_flags); | 
|---|
| 473 | env_vars.extend(base.env_vars); | 
|---|
| 474 | normalize_stderr.extend(base.normalize_stderr); | 
|---|
| 475 | normalize_stdout.extend(base.normalize_stdout); | 
|---|
| 476 | error_in_other_files.extend(base.error_in_other_files); | 
|---|
| 477 | error_matches.extend(base.error_matches); | 
|---|
| 478 | if base.require_annotations_for_level.is_some() { | 
|---|
| 479 | *require_annotations_for_level = base.require_annotations_for_level; | 
|---|
| 480 | } | 
|---|
| 481 | if base.exit_status.is_some() { | 
|---|
| 482 | *exit_status = base.exit_status; | 
|---|
| 483 | } | 
|---|
| 484 | if base.require_annotations.is_some() { | 
|---|
| 485 | *require_annotations = base.require_annotations; | 
|---|
| 486 | } | 
|---|
| 487 | if base.diagnostic_code_prefix.is_some() { | 
|---|
| 488 | *diagnostic_code_prefix = base.diagnostic_code_prefix; | 
|---|
| 489 | } | 
|---|
| 490 |  | 
|---|
| 491 | for (k, v) in base.custom { | 
|---|
| 492 | custom.insert(k, v); | 
|---|
| 493 | } | 
|---|
| 494 |  | 
|---|
| 495 | *self.base() = defaults; | 
|---|
| 496 |  | 
|---|
| 497 | if self.errors.is_empty() { | 
|---|
| 498 | Ok(self.comments) | 
|---|
| 499 | } else { | 
|---|
| 500 | Err(self.errors) | 
|---|
| 501 | } | 
|---|
| 502 | } | 
|---|
| 503 | } | 
|---|
| 504 |  | 
|---|
| 505 | impl CommentParser<Comments> { | 
|---|
| 506 | fn parse_checked_line( | 
|---|
| 507 | &mut self, | 
|---|
| 508 | fallthrough_to: Option<NonZeroUsize>, | 
|---|
| 509 | current_line: NonZeroUsize, | 
|---|
| 510 | line: Spanned<&[u8]>, | 
|---|
| 511 | ) -> std::result::Result<ParsePatternResult, Spanned<Utf8Error>> { | 
|---|
| 512 | let mut res = ParsePatternResult::Other; | 
|---|
| 513 |  | 
|---|
| 514 | if let Some((_, comment)) = | 
|---|
| 515 | line.split_once_str(self.comment_start) | 
|---|
| 516 | .filter(|(pre, c)| match &c[..] { | 
|---|
| 517 | [ b'@', ..] => pre.is_empty(), | 
|---|
| 518 | [ b'~', ..] => true, | 
|---|
| 519 | _ => false, | 
|---|
| 520 | }) | 
|---|
| 521 | { | 
|---|
| 522 | if let Some(command) = comment.strip_prefix( b"@") { | 
|---|
| 523 | self.parse_command(command.to_str()?.trim()) | 
|---|
| 524 | } else if let Some(pattern) = comment.strip_prefix( b"~") { | 
|---|
| 525 | let (revisions, pattern) = self.parse_revisions(pattern.to_str()?); | 
|---|
| 526 | self.revisioned(revisions, |this| { | 
|---|
| 527 | res = this.parse_pattern(pattern, fallthrough_to, current_line); | 
|---|
| 528 | }) | 
|---|
| 529 | } else { | 
|---|
| 530 | unreachable!() | 
|---|
| 531 | } | 
|---|
| 532 | } else { | 
|---|
| 533 | for pos in line.clone().find_iter(self.comment_start) { | 
|---|
| 534 | let (_, rest) = line.clone().to_str()?.split_at(pos + 2); | 
|---|
| 535 | for rest in std::iter::once(rest.clone()).chain(rest.strip_prefix( " ")) { | 
|---|
| 536 | let c = rest.chars().next(); | 
|---|
| 537 | if let Some(Spanned { | 
|---|
| 538 | content: '@'| '~'| '['| ']'| '^'| '|', | 
|---|
| 539 | span, | 
|---|
| 540 | }) = c | 
|---|
| 541 | { | 
|---|
| 542 | self.error( | 
|---|
| 543 | span, | 
|---|
| 544 | format!( | 
|---|
| 545 | "comment looks suspiciously like a test suite command: `{} `\n \ | 
|---|
| 546 |                              All `{} @` test suite commands must be at the start of the line.\n \ | 
|---|
| 547 |                              The `{} ` must be directly followed by `@` or `~`.", | 
|---|
| 548 | *rest, self.comment_start, self.comment_start, | 
|---|
| 549 | ), | 
|---|
| 550 | ); | 
|---|
| 551 | } else { | 
|---|
| 552 | let mut parser = Self { | 
|---|
| 553 | errors: vec![], | 
|---|
| 554 | comments: Comments::default(), | 
|---|
| 555 | commands: std::mem::take(&mut self.commands), | 
|---|
| 556 | comment_start: self.comment_start, | 
|---|
| 557 | }; | 
|---|
| 558 | let span = rest.span(); | 
|---|
| 559 | parser.parse_command(rest); | 
|---|
| 560 | if parser.errors.is_empty() { | 
|---|
| 561 | self.error( | 
|---|
| 562 | span, | 
|---|
| 563 | format!( | 
|---|
| 564 | "a compiletest-rs style comment was detected.\n \ | 
|---|
| 565 |                                 Please use text that could not also be interpreted as a command,\n \ | 
|---|
| 566 |                                 and prefix all actual commands with `{} @`", | 
|---|
| 567 | self.comment_start | 
|---|
| 568 | ), | 
|---|
| 569 | ); | 
|---|
| 570 | } | 
|---|
| 571 | self.commands = parser.commands; | 
|---|
| 572 | } | 
|---|
| 573 | } | 
|---|
| 574 | } | 
|---|
| 575 | } | 
|---|
| 576 | Ok(res) | 
|---|
| 577 | } | 
|---|
| 578 | } | 
|---|
| 579 |  | 
|---|
| 580 | impl<CommentsType> CommentParser<CommentsType> { | 
|---|
| 581 | /// Emits an [`InvalidComment`](Error::InvalidComment) error with the given span and message. | 
|---|
| 582 | pub fn error(&mut self, span: Span, s: impl Into<String>) { | 
|---|
| 583 | self.errors.push(Error::InvalidComment { | 
|---|
| 584 | msg: s.into(), | 
|---|
| 585 | span, | 
|---|
| 586 | }); | 
|---|
| 587 | } | 
|---|
| 588 |  | 
|---|
| 589 | /// Checks a condition and emits an error if it is not met. | 
|---|
| 590 | pub fn check(&mut self, span: Span, cond: bool, s: impl Into<String>) { | 
|---|
| 591 | if !cond { | 
|---|
| 592 | self.error(span, s); | 
|---|
| 593 | } | 
|---|
| 594 | } | 
|---|
| 595 |  | 
|---|
| 596 | /// Checks an option and emits an error if it is `None`. | 
|---|
| 597 | pub fn check_some<T>(&mut self, span: Span, opt: Option<T>, s: impl Into<String>) -> Option<T> { | 
|---|
| 598 | self.check(span, cond:opt.is_some(), s); | 
|---|
| 599 | opt | 
|---|
| 600 | } | 
|---|
| 601 | } | 
|---|
| 602 |  | 
|---|
| 603 | impl CommentParser<Comments> { | 
|---|
| 604 | fn parse_command(&mut self, command: Spanned<&str>) { | 
|---|
| 605 | let (revisions, command) = self.parse_revisions(command); | 
|---|
| 606 |  | 
|---|
| 607 | // Commands are letters or dashes, grab everything until the first character that is neither of those. | 
|---|
| 608 | let (command, args) = match command | 
|---|
| 609 | .char_indices() | 
|---|
| 610 | .find_map(|(i, c)| (!c.is_alphanumeric() && c != '-'&& c != '_').then_some(i)) | 
|---|
| 611 | { | 
|---|
| 612 | None => { | 
|---|
| 613 | let span = command.span().shrink_to_end(); | 
|---|
| 614 | (command, Spanned::new( "", span)) | 
|---|
| 615 | } | 
|---|
| 616 | Some(i) => { | 
|---|
| 617 | let (command, args) = command.split_at(i); | 
|---|
| 618 | // Commands are separated from their arguments by ':' | 
|---|
| 619 | let next = args | 
|---|
| 620 | .chars() | 
|---|
| 621 | .next() | 
|---|
| 622 | .expect( "the `position` above guarantees that there is at least one char"); | 
|---|
| 623 | let pos = next.len_utf8(); | 
|---|
| 624 | self.check( | 
|---|
| 625 | next.span, | 
|---|
| 626 | next.content == ':', | 
|---|
| 627 | "test command must be followed by `:` (or end the line)", | 
|---|
| 628 | ); | 
|---|
| 629 | (command, args.split_at(pos).1.trim()) | 
|---|
| 630 | } | 
|---|
| 631 | }; | 
|---|
| 632 |  | 
|---|
| 633 | if *command == "revisions"{ | 
|---|
| 634 | self.check( | 
|---|
| 635 | revisions.span(), | 
|---|
| 636 | revisions.is_empty(), | 
|---|
| 637 | "revisions cannot be declared under a revision", | 
|---|
| 638 | ); | 
|---|
| 639 | self.check( | 
|---|
| 640 | revisions.span(), | 
|---|
| 641 | self.revisions.is_none(), | 
|---|
| 642 | "cannot specify `revisions` twice", | 
|---|
| 643 | ); | 
|---|
| 644 | self.revisions = Some(args.split_whitespace().map(|s| s.to_string()).collect()); | 
|---|
| 645 | return; | 
|---|
| 646 | } | 
|---|
| 647 | self.revisioned(revisions, |this| this.parse_command(command, args)); | 
|---|
| 648 | } | 
|---|
| 649 |  | 
|---|
| 650 | fn revisioned( | 
|---|
| 651 | &mut self, | 
|---|
| 652 | revisions: Spanned<Vec<String>>, | 
|---|
| 653 | f: impl FnOnce(&mut CommentParser<&mut Revisioned>), | 
|---|
| 654 | ) { | 
|---|
| 655 | let Spanned { | 
|---|
| 656 | content: revisions, | 
|---|
| 657 | span, | 
|---|
| 658 | } = revisions; | 
|---|
| 659 | let mut this = CommentParser { | 
|---|
| 660 | comment_start: self.comment_start, | 
|---|
| 661 | errors: std::mem::take(&mut self.errors), | 
|---|
| 662 | commands: std::mem::take(&mut self.commands), | 
|---|
| 663 | comments: self | 
|---|
| 664 | .revisioned | 
|---|
| 665 | .entry(revisions) | 
|---|
| 666 | .or_insert_with(|| Revisioned { | 
|---|
| 667 | span, | 
|---|
| 668 | ..Default::default() | 
|---|
| 669 | }), | 
|---|
| 670 | }; | 
|---|
| 671 | f(&mut this); | 
|---|
| 672 | let CommentParser { | 
|---|
| 673 | errors, commands, .. | 
|---|
| 674 | } = this; | 
|---|
| 675 | self.commands = commands; | 
|---|
| 676 | self.errors = errors; | 
|---|
| 677 | } | 
|---|
| 678 | } | 
|---|
| 679 |  | 
|---|
| 680 | impl CommentParser<&mut Revisioned> { | 
|---|
| 681 | fn parse_normalize_test( | 
|---|
| 682 | &mut self, | 
|---|
| 683 | args: Spanned<&str>, | 
|---|
| 684 | mode: &str, | 
|---|
| 685 | ) -> Option<(Regex, Vec<u8>)> { | 
|---|
| 686 | let (from, rest) = self.parse_str(args); | 
|---|
| 687 |  | 
|---|
| 688 | let to = match rest.strip_prefix( "->") { | 
|---|
| 689 | Some(v) => v, | 
|---|
| 690 | None => { | 
|---|
| 691 | self.error( | 
|---|
| 692 | rest.span(), | 
|---|
| 693 | format!( | 
|---|
| 694 | "normalize-{mode} -test needs a pattern and replacement separated by `->`" | 
|---|
| 695 | ), | 
|---|
| 696 | ); | 
|---|
| 697 | return None; | 
|---|
| 698 | } | 
|---|
| 699 | } | 
|---|
| 700 | .trim_start(); | 
|---|
| 701 | let (to, rest) = self.parse_str(to); | 
|---|
| 702 |  | 
|---|
| 703 | self.check( | 
|---|
| 704 | rest.span(), | 
|---|
| 705 | rest.is_empty(), | 
|---|
| 706 | "trailing text after pattern replacement", | 
|---|
| 707 | ); | 
|---|
| 708 |  | 
|---|
| 709 | let regex = self.parse_regex(from)?.content; | 
|---|
| 710 | Some((regex, to.as_bytes().to_owned())) | 
|---|
| 711 | } | 
|---|
| 712 |  | 
|---|
| 713 | /// Adds a flag, or errors if it already existed. | 
|---|
| 714 | pub fn set_custom_once(&mut self, key: &'static str, custom: impl Flag + 'static, span: Span) { | 
|---|
| 715 | let prev = self | 
|---|
| 716 | .custom | 
|---|
| 717 | .insert(key, Spanned::new(vec![Box::new(custom)], span.clone())); | 
|---|
| 718 | self.check( | 
|---|
| 719 | span, | 
|---|
| 720 | prev.is_none(), | 
|---|
| 721 | format!( "cannot specify `{key} ` twice"), | 
|---|
| 722 | ); | 
|---|
| 723 | } | 
|---|
| 724 | } | 
|---|
| 725 |  | 
|---|
| 726 | impl CommentParser<Comments> { | 
|---|
| 727 | fn commands() -> HashMap<&'static str, CommandParserFunc> { | 
|---|
| 728 | let mut commands = HashMap::<_, CommandParserFunc>::new(); | 
|---|
| 729 | macro_rules! commands { | 
|---|
| 730 | ($($name:expr => ($this:ident, $args:ident, $span:ident)$block:block)*) => { | 
|---|
| 731 | $(commands.insert($name, |$this, $args, $span| { | 
|---|
| 732 | $block | 
|---|
| 733 | });)* | 
|---|
| 734 | }; | 
|---|
| 735 | } | 
|---|
| 736 | commands! { | 
|---|
| 737 | "compile-flags"=> (this, args, _span){ | 
|---|
| 738 | if let Some(parsed) = comma::parse_command(*args) { | 
|---|
| 739 | this.compile_flags.extend(parsed); | 
|---|
| 740 | } else { | 
|---|
| 741 | this.error(args.span(), format!( "`{} ` contains an unclosed quotation mark", *args)); | 
|---|
| 742 | } | 
|---|
| 743 | } | 
|---|
| 744 | "rustc-env"=> (this, args, _span){ | 
|---|
| 745 | for env in args.split_whitespace() { | 
|---|
| 746 | if let Some((k, v)) = this.check_some( | 
|---|
| 747 | args.span(), | 
|---|
| 748 | env.split_once( '='), | 
|---|
| 749 | "environment variables must be key/value pairs separated by a `=`", | 
|---|
| 750 | ) { | 
|---|
| 751 | this.env_vars.push((k.to_string(), v.to_string())); | 
|---|
| 752 | } | 
|---|
| 753 | } | 
|---|
| 754 | } | 
|---|
| 755 | "normalize-stderr-test"=> (this, args, _span){ | 
|---|
| 756 | if let Some((regex, replacement)) = this.parse_normalize_test(args, "stderr") { | 
|---|
| 757 | this.normalize_stderr.push((regex.into(), replacement)) | 
|---|
| 758 | } | 
|---|
| 759 | } | 
|---|
| 760 | "normalize-stdout-test"=> (this, args, _span){ | 
|---|
| 761 | if let Some((regex, replacement)) = this.parse_normalize_test(args, "stdout") { | 
|---|
| 762 | this.normalize_stdout.push((regex.into(), replacement)) | 
|---|
| 763 | } | 
|---|
| 764 | } | 
|---|
| 765 | "error-pattern"=> (this, _args, span){ | 
|---|
| 766 | this.error(span, "`error-pattern` has been renamed to `error-in-other-file`"); | 
|---|
| 767 | } | 
|---|
| 768 | "error-in-other-file"=> (this, args, _span){ | 
|---|
| 769 | let args = args.trim(); | 
|---|
| 770 | let pat = this.parse_error_pattern(args); | 
|---|
| 771 | this.error_in_other_files.push(pat); | 
|---|
| 772 | } | 
|---|
| 773 | "stderr-per-bitwidth"=> (this, _args, span){ | 
|---|
| 774 | // args are ignored (can be used as comment) | 
|---|
| 775 | this.check( | 
|---|
| 776 | span, | 
|---|
| 777 | !this.stderr_per_bitwidth, | 
|---|
| 778 | "cannot specify `stderr-per-bitwidth` twice", | 
|---|
| 779 | ); | 
|---|
| 780 | this.stderr_per_bitwidth = true; | 
|---|
| 781 | } | 
|---|
| 782 | "run-rustfix"=> (this, _args, span){ | 
|---|
| 783 | this.error(span, "rustfix is now ran by default when applicable suggestions are found"); | 
|---|
| 784 | } | 
|---|
| 785 | "check-pass"=> (this, _args, span){ | 
|---|
| 786 | _ = this.exit_status.set(0, span.clone()); | 
|---|
| 787 | this.require_annotations = Spanned::new(false, span.clone()).into(); | 
|---|
| 788 | } | 
|---|
| 789 | "require-annotations-for-level"=> (this, args, span){ | 
|---|
| 790 | let args = args.trim(); | 
|---|
| 791 | let prev = match args.content.parse() { | 
|---|
| 792 | Ok(it) =>  this.require_annotations_for_level.set(it, args.span()), | 
|---|
| 793 | Err(msg) => { | 
|---|
| 794 | this.error(args.span(), msg); | 
|---|
| 795 | None | 
|---|
| 796 | }, | 
|---|
| 797 | }; | 
|---|
| 798 |  | 
|---|
| 799 | this.check( | 
|---|
| 800 | span, | 
|---|
| 801 | prev.is_none(), | 
|---|
| 802 | "cannot specify `require-annotations-for-level` twice", | 
|---|
| 803 | ); | 
|---|
| 804 | } | 
|---|
| 805 | } | 
|---|
| 806 | commands | 
|---|
| 807 | } | 
|---|
| 808 | } | 
|---|
| 809 |  | 
|---|
| 810 | impl CommentParser<&mut Revisioned> { | 
|---|
| 811 | fn parse_command(&mut self, command: Spanned<&str>, args: Spanned<&str>) { | 
|---|
| 812 | if let Some(command_handler) = self.commands.get(*command) { | 
|---|
| 813 | command_handler(self, args, command.span()); | 
|---|
| 814 | } else if let Some(rest) = command | 
|---|
| 815 | .strip_prefix( "ignore-") | 
|---|
| 816 | .or_else(|| command.strip_prefix( "only-")) | 
|---|
| 817 | { | 
|---|
| 818 | // args are ignored (can be used as comment) | 
|---|
| 819 | match Condition::parse(*rest, *args) { | 
|---|
| 820 | Ok(cond) => { | 
|---|
| 821 | if command.starts_with( "ignore") { | 
|---|
| 822 | self.ignore.push(cond) | 
|---|
| 823 | } else { | 
|---|
| 824 | self.only.push(cond) | 
|---|
| 825 | } | 
|---|
| 826 | } | 
|---|
| 827 | Err(msg) => self.error(rest.span(), msg), | 
|---|
| 828 | } | 
|---|
| 829 | } else { | 
|---|
| 830 | let best_match = self | 
|---|
| 831 | .commands | 
|---|
| 832 | .keys() | 
|---|
| 833 | .min_by_key(|key| levenshtein::levenshtein(key, *command)) | 
|---|
| 834 | .unwrap(); | 
|---|
| 835 | self.error( | 
|---|
| 836 | command.span(), | 
|---|
| 837 | format!( | 
|---|
| 838 | "`{} ` is not a command known to `ui_test`, did you mean `{best_match} `?", | 
|---|
| 839 | *command | 
|---|
| 840 | ), | 
|---|
| 841 | ); | 
|---|
| 842 | } | 
|---|
| 843 | } | 
|---|
| 844 | } | 
|---|
| 845 |  | 
|---|
| 846 | impl<CommentsType> CommentParser<CommentsType> { | 
|---|
| 847 | fn parse_regex(&mut self, regex: Spanned<&str>) -> Option<Spanned<Regex>> { | 
|---|
| 848 | match Regex::new(*regex) { | 
|---|
| 849 | Ok(r) => Some(regex.map(|_| r)), | 
|---|
| 850 | Err(err) => { | 
|---|
| 851 | self.error(regex.span(), format!( "invalid regex: {err:?} ")); | 
|---|
| 852 | None | 
|---|
| 853 | } | 
|---|
| 854 | } | 
|---|
| 855 | } | 
|---|
| 856 |  | 
|---|
| 857 | /// Parses a string literal. `s` has to start with `"`; everything until the next `"` is | 
|---|
| 858 | /// returned in the first component. `\` can be used to escape arbitrary character. | 
|---|
| 859 | /// Second return component is the rest of the string with leading whitespace removed. | 
|---|
| 860 | fn parse_str<'a>(&mut self, s: Spanned<&'a str>) -> (Spanned<&'a str>, Spanned<&'a str>) { | 
|---|
| 861 | match s.strip_prefix( "\" ") { | 
|---|
| 862 | Some(s) => { | 
|---|
| 863 | let mut escaped = false; | 
|---|
| 864 | for (i, c) in s.char_indices() { | 
|---|
| 865 | if escaped { | 
|---|
| 866 | // Accept any character as literal after a `\`. | 
|---|
| 867 | escaped = false; | 
|---|
| 868 | } else if c == '"'{ | 
|---|
| 869 | let (a, b) = s.split_at(i); | 
|---|
| 870 | let b = b.split_at(1).1; | 
|---|
| 871 | return (a, b.trim_start()); | 
|---|
| 872 | } else { | 
|---|
| 873 | escaped = c == '\\ '; | 
|---|
| 874 | } | 
|---|
| 875 | } | 
|---|
| 876 | self.error(s.span(), format!( "no closing quotes found for {} ", *s)); | 
|---|
| 877 | let span = s.span(); | 
|---|
| 878 | (s, Spanned::new( "", span)) | 
|---|
| 879 | } | 
|---|
| 880 | None => { | 
|---|
| 881 | if s.is_empty() { | 
|---|
| 882 | self.error(s.span(), "expected quoted string, but found end of line") | 
|---|
| 883 | } else { | 
|---|
| 884 | let c = s.chars().next().unwrap(); | 
|---|
| 885 | self.error(c.span, format!( "expected `\" `, got `{} `", c.content)) | 
|---|
| 886 | } | 
|---|
| 887 | let span = s.span(); | 
|---|
| 888 | (s, Spanned::new( "", span)) | 
|---|
| 889 | } | 
|---|
| 890 | } | 
|---|
| 891 | } | 
|---|
| 892 |  | 
|---|
| 893 | // parse something like \[[a-z]+(,[a-z]+)*\] | 
|---|
| 894 | fn parse_revisions<'a>( | 
|---|
| 895 | &mut self, | 
|---|
| 896 | pattern: Spanned<&'a str>, | 
|---|
| 897 | ) -> (Spanned<Vec<String>>, Spanned<&'a str>) { | 
|---|
| 898 | match pattern.strip_prefix( "[") { | 
|---|
| 899 | Some(s) => { | 
|---|
| 900 | // revisions | 
|---|
| 901 | let end = s.char_indices().find_map(|(i, c)| match c { | 
|---|
| 902 | ']'=> Some(i), | 
|---|
| 903 | _ => None, | 
|---|
| 904 | }); | 
|---|
| 905 | let Some(end) = end else { | 
|---|
| 906 | self.error(s.span(), "`[` without corresponding `]`"); | 
|---|
| 907 | return ( | 
|---|
| 908 | Spanned::new(vec![], pattern.span().shrink_to_start()), | 
|---|
| 909 | pattern, | 
|---|
| 910 | ); | 
|---|
| 911 | }; | 
|---|
| 912 | let (revision, pattern) = s.split_at(end); | 
|---|
| 913 | let revisions = revision.split( ',').map(|s| s.trim().to_string()).collect(); | 
|---|
| 914 | ( | 
|---|
| 915 | Spanned::new(revisions, revision.span()), | 
|---|
| 916 | // 1.. because `split_at` includes the separator | 
|---|
| 917 | pattern.split_at(1).1.trim_start(), | 
|---|
| 918 | ) | 
|---|
| 919 | } | 
|---|
| 920 | _ => ( | 
|---|
| 921 | Spanned::new(vec![], pattern.span().shrink_to_start()), | 
|---|
| 922 | pattern, | 
|---|
| 923 | ), | 
|---|
| 924 | } | 
|---|
| 925 | } | 
|---|
| 926 | } | 
|---|
| 927 |  | 
|---|
| 928 | impl CommentParser<&mut Revisioned> { | 
|---|
| 929 | // parse something like: | 
|---|
| 930 | // (\[[a-z]+(,[a-z]+)*\])? | 
|---|
| 931 | // (?P<offset>\||[\^]+)? * | 
|---|
| 932 | // ((?P<level>ERROR|HELP|WARN|NOTE): (?P<text>.*))|(?P<code>[a-z0-9_:]+) | 
|---|
| 933 | fn parse_pattern( | 
|---|
| 934 | &mut self, | 
|---|
| 935 | pattern: Spanned<&str>, | 
|---|
| 936 | fallthrough_to: Option<NonZeroUsize>, | 
|---|
| 937 | current_line: NonZeroUsize, | 
|---|
| 938 | ) -> ParsePatternResult { | 
|---|
| 939 | let c = pattern.chars().next(); | 
|---|
| 940 | let mut res = ParsePatternResult::Other; | 
|---|
| 941 |  | 
|---|
| 942 | let (match_line, pattern) = match c { | 
|---|
| 943 | Some(Spanned { content: '|', span }) => ( | 
|---|
| 944 | match fallthrough_to { | 
|---|
| 945 | Some(match_line) => { | 
|---|
| 946 | res = ParsePatternResult::ErrorAbove { match_line }; | 
|---|
| 947 | match_line | 
|---|
| 948 | } | 
|---|
| 949 | None => { | 
|---|
| 950 | res = ParsePatternResult::Fallthrough { | 
|---|
| 951 | span, | 
|---|
| 952 | idx: self.error_matches.len(), | 
|---|
| 953 | }; | 
|---|
| 954 | current_line | 
|---|
| 955 | } | 
|---|
| 956 | }, | 
|---|
| 957 | pattern.split_at(1).1, | 
|---|
| 958 | ), | 
|---|
| 959 | Some(Spanned { | 
|---|
| 960 | content: '^', | 
|---|
| 961 | span: _, | 
|---|
| 962 | }) => { | 
|---|
| 963 | let offset = pattern.chars().take_while(|c| c.content == '^').count(); | 
|---|
| 964 | match current_line | 
|---|
| 965 | .get() | 
|---|
| 966 | .checked_sub(offset) | 
|---|
| 967 | .and_then(NonZeroUsize::new) | 
|---|
| 968 | { | 
|---|
| 969 | // lines are one-indexed, so a target line of 0 is invalid, but also | 
|---|
| 970 | // prevented via `NonZeroUsize` | 
|---|
| 971 | Some(match_line) => { | 
|---|
| 972 | res = ParsePatternResult::ErrorAbove { match_line }; | 
|---|
| 973 | (match_line, pattern.split_at(offset).1) | 
|---|
| 974 | } | 
|---|
| 975 | _ => { | 
|---|
| 976 | self.error(pattern.span(), format!( | 
|---|
| 977 | "{} ~^ pattern is trying to refer to {}  lines above, but there are only {}  lines above", | 
|---|
| 978 | self.comment_start, | 
|---|
| 979 | offset, | 
|---|
| 980 | current_line.get() - 1, | 
|---|
| 981 | )); | 
|---|
| 982 | return ParsePatternResult::ErrorAbove { | 
|---|
| 983 | match_line: current_line, | 
|---|
| 984 | }; | 
|---|
| 985 | } | 
|---|
| 986 | } | 
|---|
| 987 | } | 
|---|
| 988 | Some(Spanned { | 
|---|
| 989 | content: 'v', | 
|---|
| 990 | span: _, | 
|---|
| 991 | }) => { | 
|---|
| 992 | let offset = pattern.chars().take_while(|c| c.content == 'v').count(); | 
|---|
| 993 | match current_line | 
|---|
| 994 | .get() | 
|---|
| 995 | .checked_add(offset) | 
|---|
| 996 | .and_then(NonZeroUsize::new) | 
|---|
| 997 | { | 
|---|
| 998 | Some(match_line) => { | 
|---|
| 999 | res = ParsePatternResult::ErrorBelow { | 
|---|
| 1000 | span: pattern.span(), | 
|---|
| 1001 | match_line, | 
|---|
| 1002 | }; | 
|---|
| 1003 | (match_line, pattern.split_at(offset).1) | 
|---|
| 1004 | } | 
|---|
| 1005 | _ => { | 
|---|
| 1006 | // The line count of the file is not yet known so we can only check | 
|---|
| 1007 | // if the resulting line is in the range of a usize. | 
|---|
| 1008 | self.error(pattern.span(), format!( | 
|---|
| 1009 | "{} ~v pattern is trying to refer to {}  lines below, which is more than ui_test can count", | 
|---|
| 1010 | self.comment_start, | 
|---|
| 1011 | offset, | 
|---|
| 1012 | )); | 
|---|
| 1013 | return ParsePatternResult::ErrorBelow { | 
|---|
| 1014 | span: pattern.span(), | 
|---|
| 1015 | match_line: current_line, | 
|---|
| 1016 | }; | 
|---|
| 1017 | } | 
|---|
| 1018 | } | 
|---|
| 1019 | } | 
|---|
| 1020 | Some(_) => (current_line, pattern), | 
|---|
| 1021 | None => { | 
|---|
| 1022 | self.error(pattern.span(), "no pattern specified"); | 
|---|
| 1023 | return res; | 
|---|
| 1024 | } | 
|---|
| 1025 | }; | 
|---|
| 1026 |  | 
|---|
| 1027 | let pattern = pattern.trim_start(); | 
|---|
| 1028 | let offset = pattern | 
|---|
| 1029 | .bytes() | 
|---|
| 1030 | .position(|c| !(c.is_ascii_alphanumeric() || c == b'_'|| c == b':')) | 
|---|
| 1031 | .unwrap_or(pattern.len()); | 
|---|
| 1032 |  | 
|---|
| 1033 | let (level_or_code, pattern) = pattern.split_at(offset); | 
|---|
| 1034 | if let Some(level) = level_or_code.strip_suffix( ":") { | 
|---|
| 1035 | let level = match (*level).parse() { | 
|---|
| 1036 | Ok(level) => level, | 
|---|
| 1037 | Err(msg) => { | 
|---|
| 1038 | self.error(level.span(), msg); | 
|---|
| 1039 | return res; | 
|---|
| 1040 | } | 
|---|
| 1041 | }; | 
|---|
| 1042 |  | 
|---|
| 1043 | let pattern = pattern.trim(); | 
|---|
| 1044 |  | 
|---|
| 1045 | self.check(pattern.span(), !pattern.is_empty(), "no pattern specified"); | 
|---|
| 1046 |  | 
|---|
| 1047 | let pattern = self.parse_error_pattern(pattern); | 
|---|
| 1048 |  | 
|---|
| 1049 | self.error_matches.push(ErrorMatch { | 
|---|
| 1050 | kind: ErrorMatchKind::Pattern { pattern, level }, | 
|---|
| 1051 | line: match_line, | 
|---|
| 1052 | }); | 
|---|
| 1053 | } else if (*level_or_code).parse::<Level>().is_ok() { | 
|---|
| 1054 | // Shouldn't conflict with any real diagnostic code | 
|---|
| 1055 | self.error(level_or_code.span(), "no `:` after level found"); | 
|---|
| 1056 | return res; | 
|---|
| 1057 | } else if !pattern.trim_start().is_empty() { | 
|---|
| 1058 | self.error( | 
|---|
| 1059 | pattern.span(), | 
|---|
| 1060 | format!( "text found after error code `{} `", *level_or_code), | 
|---|
| 1061 | ); | 
|---|
| 1062 | return res; | 
|---|
| 1063 | } else { | 
|---|
| 1064 | self.error_matches.push(ErrorMatch { | 
|---|
| 1065 | kind: ErrorMatchKind::Code(Spanned::new( | 
|---|
| 1066 | level_or_code.to_string(), | 
|---|
| 1067 | level_or_code.span(), | 
|---|
| 1068 | )), | 
|---|
| 1069 | line: match_line, | 
|---|
| 1070 | }); | 
|---|
| 1071 | }; | 
|---|
| 1072 |  | 
|---|
| 1073 | res | 
|---|
| 1074 | } | 
|---|
| 1075 | } | 
|---|
| 1076 |  | 
|---|
| 1077 | impl Pattern { | 
|---|
| 1078 | pub(crate) fn matches(&self, message: &str) -> bool { | 
|---|
| 1079 | match self { | 
|---|
| 1080 | Pattern::SubString(s: &String) => message.contains(s), | 
|---|
| 1081 | Pattern::Regex(r: &Regex) => r.is_match(haystack:message.as_bytes()), | 
|---|
| 1082 | } | 
|---|
| 1083 | } | 
|---|
| 1084 | } | 
|---|
| 1085 |  | 
|---|
| 1086 | impl<CommentsType> CommentParser<CommentsType> { | 
|---|
| 1087 | fn parse_error_pattern(&mut self, pattern: Spanned<&str>) -> Spanned<Pattern> { | 
|---|
| 1088 | if let Some(regex: Spanned<&str>) = pattern.strip_prefix( "/") { | 
|---|
| 1089 | match regex.strip_suffix( "/") { | 
|---|
| 1090 | Some(regex: Spanned<&str>) => match self.parse_regex(regex) { | 
|---|
| 1091 | Some(r: Spanned) => r.map(Pattern::Regex), | 
|---|
| 1092 | None => pattern.map(|p: &str| Pattern::SubString(p.to_string())), | 
|---|
| 1093 | }, | 
|---|
| 1094 | None => { | 
|---|
| 1095 | self.error( | 
|---|
| 1096 | regex.span(), | 
|---|
| 1097 | s: "expected regex pattern due to leading `/`, but found no closing `/`", | 
|---|
| 1098 | ); | 
|---|
| 1099 | pattern.map(|p: &str| Pattern::SubString(p.to_string())) | 
|---|
| 1100 | } | 
|---|
| 1101 | } | 
|---|
| 1102 | } else { | 
|---|
| 1103 | pattern.map(|p: &str| Pattern::SubString(p.to_string())) | 
|---|
| 1104 | } | 
|---|
| 1105 | } | 
|---|
| 1106 | } | 
|---|
| 1107 |  | 
|---|