diff options
Diffstat (limited to 'support/texlab/src/parser/build_log.rs')
-rw-r--r-- | support/texlab/src/parser/build_log.rs | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/support/texlab/src/parser/build_log.rs b/support/texlab/src/parser/build_log.rs new file mode 100644 index 0000000000..5f99129cdb --- /dev/null +++ b/support/texlab/src/parser/build_log.rs @@ -0,0 +1,184 @@ +use std::{cmp::Ordering, path::PathBuf}; + +use once_cell::sync::Lazy; +use regex::{Match, Regex}; + +use crate::syntax::{BuildError, BuildErrorLevel, BuildLog}; + +const MAX_LINE_LENGTH: usize = 79; + +static PACKAGE_MESSAGE_REGEX: Lazy<Regex> = + Lazy::new(|| Regex::new("^\\([a-zA-Z_\\-]+\\)\\s*(?P<msg>.*)$").unwrap()); + +static FILE_REGEX: Lazy<Regex> = + Lazy::new(|| Regex::new("\\((?P<file>[^\r\n()]+\\.(tex|sty|cls))").unwrap()); + +static TEX_ERROR_REGEX: Lazy<Regex> = Lazy::new(|| { + Regex::new("(?m)^! ((?P<msg1>(.|\r|\n)*?)\r?\nl\\.(?P<line>\\d+)( (\\.\\.\\.)?(?P<hint>[^\r\n]+))?|(?P<msg2>[^\r\n]*))").unwrap() +}); + +static WARNING_REGEX: Lazy<Regex> = + Lazy::new(|| Regex::new("(LaTeX|Package [a-zA-Z_\\-]+) Warning: (?P<msg>[^\r\n]*)").unwrap()); + +static BAD_BOX_REGEX: Lazy<Regex> = Lazy::new(|| { + Regex::new("(?P<msg>(Ov|Und)erfull \\\\[hv]box[^\r\n]*lines? (?P<line>\\d+)[^\r\n]*)").unwrap() +}); + +pub fn parse_build_log(log: &str) -> BuildLog { + let log = prepare_log(log); + let mut ranges: Vec<FileRange> = FILE_REGEX + .find_iter(&log) + .map(|result| FileRange::create(&log, result)) + .collect(); + ranges.sort(); + + let tex_errors = extract_matches(&log, &ranges, &TEX_ERROR_REGEX, BuildErrorLevel::Error); + let warnings = extract_matches(&log, &ranges, &WARNING_REGEX, BuildErrorLevel::Warning); + let bad_boxes = extract_matches(&log, &ranges, &BAD_BOX_REGEX, BuildErrorLevel::Warning); + + let errors = vec![tex_errors, warnings, bad_boxes].concat(); + BuildLog { errors } +} + +fn extract_matches( + log: &str, + ranges: &[FileRange], + regex: &Regex, + level: BuildErrorLevel, +) -> Vec<BuildError> { + let mut errors = Vec::new(); + for result in regex.find_iter(log) { + let captures = regex.captures(&log[result.start()..result.end()]).unwrap(); + let message = captures + .name("msg") + .or_else(|| captures.name("msg1")) + .or_else(|| captures.name("msg2")) + .unwrap() + .as_str() + .lines() + .next() + .unwrap_or_default() + .to_owned(); + + if let Some(range) = ranges.iter().find(|range| range.contains(result.start())) { + let line = captures + .name("line") + .map(|result| result.as_str().parse::<u32>().unwrap() - 1); + + let hint: Option<String> = if line.is_some() { + captures + .name("hint") + .map(|r| String::from(r.as_str().trim())) + } else { + None + }; + + errors.push(BuildError { + relative_path: range.path.clone(), + level, + message, + line, + hint, + }); + } + } + errors +} + +fn prepare_log(log: &str) -> String { + let mut old_lines = log.lines(); + let mut new_lines: Vec<String> = Vec::new(); + while let Some(line) = old_lines.next() { + if PACKAGE_MESSAGE_REGEX.is_match(line) { + let captures = PACKAGE_MESSAGE_REGEX.captures(line).unwrap(); + if let Some(last_line) = new_lines.last_mut() { + last_line.push(' '); + last_line.push_str(captures.name("msg").unwrap().as_str()); + } + } else if line.ends_with("...") { + let mut new_line = line[line.len() - 3..].to_owned(); + if let Some(old_line) = old_lines.next() { + new_line.push_str(old_line); + } + new_lines.push(new_line); + } else if line.chars().count() == MAX_LINE_LENGTH { + let mut new_line = String::new(); + new_line.push_str(line); + if let Some(old_line) = old_lines.next() { + new_line.push_str(old_line); + } + new_lines.push(new_line); + } else { + new_lines.push(line.to_owned()); + } + } + new_lines.join("\n") +} + +#[derive(Debug, PartialEq, Eq, Clone)] +struct FileRange { + pub path: PathBuf, + pub start: usize, + pub end: usize, +} + +impl FileRange { + fn create(log: &str, result: Match) -> Self { + let mut balance = 1; + let mut end = result.start() + 1; + let chars = log[result.start() + 1..].chars(); + for c in chars { + if balance <= 0 { + break; + } + + if c == '(' { + balance += 1; + } else if c == ')' { + balance -= 1; + } + end += c.len_utf8(); + } + + let captures = FILE_REGEX.captures(result.as_str()).unwrap(); + let path = PathBuf::from(captures.name("file").unwrap().as_str()); + Self { + path, + start: result.start(), + end, + } + } + + fn len(&self) -> usize { + self.end - self.start + 1 + } + + fn contains(&self, index: usize) -> bool { + index >= self.start && index <= self.end + } +} + +impl Ord for FileRange { + fn cmp(&self, other: &Self) -> Ordering { + self.len().cmp(&other.len()) + } +} + +impl PartialOrd for FileRange { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +#[cfg(test)] +mod tests { + use super::parse_build_log; + + #[test] + fn test_parse() { + insta::glob!("test_data/build_log/*.txt", |path| { + let text = std::fs::read_to_string(path).unwrap().replace("\r\n", "\n"); + insta::assert_debug_snapshot!(parse_build_log(&text)); + }); + } +} |