use std::borrow::Cow;
use std::cmp::Ordering;
use crate::Issue;
use crate::IssueCollection;
use crate::Level;
use crate::formatter::FormatterConfig;
pub struct LazyFilteredIssues<'issues> {
iter: std::slice::Iter<'issues, Issue>,
min_level: Option<Level>,
filter_fixable: bool,
}
pub enum FilteredIssues<'issues> {
Lazy(LazyFilteredIssues<'issues>),
Sorted(std::vec::IntoIter<&'issues Issue>),
}
impl<'issues> Iterator for LazyFilteredIssues<'issues> {
type Item = &'issues Issue;
#[inline]
fn next(&mut self) -> Option<&'issues Issue> {
for issue in self.iter.by_ref() {
if let Some(min) = self.min_level
&& issue.level < min
{
continue;
}
if self.filter_fixable && issue.edits.is_empty() {
continue;
}
return Some(issue);
}
None
}
}
impl<'issues> Iterator for FilteredIssues<'issues> {
type Item = &'issues Issue;
#[inline]
fn next(&mut self) -> Option<&'issues Issue> {
match self {
Self::Lazy(it) => it.next(),
Self::Sorted(it) => it.next(),
}
}
}
#[inline]
pub fn filter_issues<'issues>(
issues: &'issues IssueCollection,
config: &FormatterConfig,
sortable: bool,
) -> FilteredIssues<'issues> {
let min_level = config.minimum_level;
let filter_fixable = config.filter_fixable;
let lazy = LazyFilteredIssues { iter: issues.issues.iter(), min_level, filter_fixable };
if sortable && config.sort {
let mut refs: Vec<&Issue> = lazy.collect();
refs.sort_by(compare_issues);
FilteredIssues::Sorted(refs.into_iter())
} else {
FilteredIssues::Lazy(lazy)
}
}
pub(crate) fn utf8_preserving_byte_offsets(bytes: &[u8]) -> Cow<'_, str> {
if let Ok(source) = std::str::from_utf8(bytes) {
return Cow::Borrowed(source);
}
let mut source = String::with_capacity(bytes.len());
let mut offset = 0;
while offset < bytes.len() {
match std::str::from_utf8(&bytes[offset..]) {
Ok(suffix) => {
source.push_str(suffix);
break;
}
Err(error) => {
let valid_end = offset + error.valid_up_to();
source.push_str(String::from_utf8_lossy(&bytes[offset..valid_end]).as_ref());
let invalid_length = error.error_len().unwrap_or(bytes.len() - valid_end);
for _ in 0..invalid_length {
source.push('?');
}
offset = valid_end + invalid_length;
}
}
}
Cow::Owned(source)
}
#[inline]
fn compare_issues(a: &&Issue, b: &&Issue) -> Ordering {
match a.level.cmp(&b.level) {
Ordering::Less => Ordering::Less,
Ordering::Greater => Ordering::Greater,
Ordering::Equal => match a.code.as_deref().cmp(&b.code.as_deref()) {
Ordering::Less => Ordering::Less,
Ordering::Greater => Ordering::Greater,
Ordering::Equal => match (a.primary_span(), b.primary_span()) {
(Some(a_span), Some(b_span)) => a_span.cmp(&b_span),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => Ordering::Equal,
},
},
}
}
pub fn xml_encode(input: impl AsRef<str>) -> String {
let input = input.as_ref();
let mut result = String::with_capacity(input.len());
for c in input.chars() {
if !is_xml_1_0_character(c) {
continue;
}
let next = match c {
'&' => "&",
'<' => "<",
'>' => ">",
'"' => """,
'\'' => "'",
'\n' => " ",
'\r' => " ",
_ => {
result.push(c);
continue;
}
};
result.push_str(next);
}
result
}
#[inline]
fn is_xml_1_0_character(character: char) -> bool {
matches!(
character,
'\u{9}' | '\u{A}' | '\u{D}' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}'
)
}
pub fn long_message(issue: &Issue, include_annotations: bool) -> String {
let mut message = issue.message.clone();
if include_annotations {
for annotation in &issue.annotations {
if let Some(annotation_msg) = annotation.message.as_ref() {
message.push('\n');
message.push('>');
message.push_str(annotation_msg.as_str());
}
}
}
if !issue.notes.is_empty() {
message.push('\n');
for note in &issue.notes {
message.push('\n');
message.push_str(note.as_str());
}
}
if let Some(help) = issue.help.as_ref() {
message.push_str("\n\nHelp: ");
message.push_str(help.as_str());
}
if let Some(link) = issue.link.as_ref() {
message.push_str("\n\nMore information: ");
message.push_str(link.as_str());
}
message
}
#[must_use]
pub fn osc8_hyperlink(template: &str, abs_path: &str, line: u32, column: u32, display_text: &str) -> String {
let url = template
.replace("%file%", &strip_windows_verbatim_prefix(abs_path))
.replace("%line%", &line.to_string())
.replace("%column%", &column.to_string());
format!("\x1b]8;;{url}\x1b\\{display_text}\x1b]8;;\x1b\\")
}
fn strip_windows_verbatim_prefix(path: &str) -> std::borrow::Cow<'_, str> {
if let Some(rest) = path.strip_prefix(r"\\?\UNC\") {
std::borrow::Cow::Owned(format!(r"\\{rest}"))
} else if let Some(rest) = path.strip_prefix(r"\\?\") {
std::borrow::Cow::Borrowed(rest)
} else {
std::borrow::Cow::Borrowed(path)
}
}
#[cfg(test)]
mod tests {
use super::strip_windows_verbatim_prefix;
use super::utf8_preserving_byte_offsets;
use super::xml_encode;
#[test]
fn preserves_byte_offsets_when_replacing_invalid_utf8() {
let bytes = b"valid \xc2\xa9 invalid \xa9 truncated \xf0\x9f\nnext";
let source = utf8_preserving_byte_offsets(bytes);
assert_eq!(source.len(), bytes.len());
assert_eq!(source.as_bytes(), b"valid \xc2\xa9 invalid ? truncated ??\nnext");
}
#[test]
fn xml_encoding_escapes_markup_and_line_boundaries() {
assert_eq!(xml_encode("<&>\"'\r\n"), "<&>"' ");
}
#[test]
fn xml_encoding_removes_characters_forbidden_by_xml_1_0() {
assert_eq!(xml_encode("a\0\u{1}\u{B}\u{C}\u{1F}\u{FFFE}\u{FFFF}b\t"), "ab\t");
}
#[test]
fn strips_verbatim_drive_prefix() {
assert_eq!(strip_windows_verbatim_prefix(r"\\?\C:\Users\foo\bar.php"), r"C:\Users\foo\bar.php");
}
#[test]
fn strips_verbatim_unc_prefix() {
assert_eq!(strip_windows_verbatim_prefix(r"\\?\UNC\server\share\file.php"), r"\\server\share\file.php");
}
#[test]
fn leaves_plain_windows_paths_unchanged() {
assert_eq!(strip_windows_verbatim_prefix(r"C:\Users\foo\bar.php"), r"C:\Users\foo\bar.php");
}
#[test]
fn leaves_unix_paths_unchanged() {
assert_eq!(strip_windows_verbatim_prefix("/home/foo/bar.php"), "/home/foo/bar.php");
}
#[test]
fn leaves_unc_without_verbatim_unchanged() {
assert_eq!(strip_windows_verbatim_prefix(r"\\server\share\file.php"), r"\\server\share\file.php");
}
}