use super::Location;
use std::borrow::Cow;
use std::cell::Ref;
use std::ops::Range;
#[cfg(test)]
use std::rc::Rc;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum ReportType {
#[default]
None,
Error,
Warning,
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum SpanRole<'a> {
Primary { label: Cow<'a, str> },
Supplementary { label: Cow<'a, str> },
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Span<'a> {
pub range: Range<usize>,
pub role: SpanRole<'a>,
}
#[derive(Debug)]
pub struct Snippet<'a> {
pub code: &'a super::Code,
code_string: Ref<'a, str>,
pub spans: Vec<Span<'a>>,
}
impl Snippet<'_> {
#[must_use]
pub fn with_code(code: &super::Code) -> Snippet<'_> {
Self::with_code_and_spans(code, Vec::new())
}
#[must_use]
pub fn with_code_and_spans<'a>(code: &'a super::Code, spans: Vec<Span<'a>>) -> Snippet<'a> {
Snippet {
code,
code_string: Ref::map(code.value.borrow(), String::as_str),
spans,
}
}
#[must_use]
pub fn with_primary_span<'a>(location: &'a Location, label: Cow<'a, str>) -> Vec<Snippet<'a>> {
let range = location.byte_range();
let role = SpanRole::Primary { label };
let spans = vec![Span { range, role }];
let mut snippets = vec![Snippet::with_code_and_spans(&location.code, spans)];
location.code.source.extend_with_context(&mut snippets);
snippets
}
#[inline(always)]
#[must_use]
pub fn code_string(&self) -> &str {
&self.code_string
}
}
impl Clone for Snippet<'_> {
fn clone(&self) -> Self {
Snippet {
code: self.code,
code_string: Ref::clone(&self.code_string),
spans: self.spans.clone(),
}
}
}
impl PartialEq<Snippet<'_>> for Snippet<'_> {
fn eq(&self, other: &Snippet<'_>) -> bool {
self.code == other.code && self.spans == other.spans
}
}
impl Eq for Snippet<'_> {}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum FootnoteType {
#[default]
None,
Note,
Suggestion,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Footnote<'a> {
pub r#type: FootnoteType,
pub label: Cow<'a, str>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub struct Report<'a> {
pub r#type: ReportType,
pub id: Option<Cow<'a, str>>,
pub title: Cow<'a, str>,
pub snippets: Vec<Snippet<'a>>,
pub footnotes: Vec<Footnote<'a>>,
}
impl Report<'_> {
#[inline]
#[must_use]
pub fn new() -> Self {
Report::default()
}
}
pub fn snippet_for_code<'a, 'b>(
snippets: &'b mut Vec<Snippet<'a>>,
code: &'a super::Code,
) -> &'b mut Snippet<'a> {
if let Some(i) = snippets.iter().position(|s| std::ptr::eq(s.code, code)) {
&mut snippets[i]
} else {
snippets.push(Snippet::with_code(code));
snippets.last_mut().unwrap()
}
}
pub fn add_span<'a>(code: &'a super::Code, span: Span<'a>, snippets: &mut Vec<Snippet<'a>>) {
snippet_for_code(snippets, code).spans.push(span);
}
#[test]
fn test_add_span_with_matching_code() {
let code = Rc::new(super::Code {
value: std::cell::RefCell::new("echo hello".to_string()),
start_line_number: std::num::NonZero::new(1).unwrap(),
source: Rc::new(super::Source::CommandString),
});
let span = Span {
range: 5..10,
role: SpanRole::Primary {
label: "greeting".into(),
},
};
let mut snippets = vec![Snippet::with_code(&code)];
add_span(&code, span, &mut snippets);
assert_eq!(snippets.len(), 1);
assert_eq!(snippets[0].spans.len(), 1);
assert_eq!(snippets[0].spans[0].range, 5..10);
assert_eq!(
snippets[0].spans[0].role,
SpanRole::Primary {
label: "greeting".into()
}
);
}
#[test]
fn test_add_span_without_matching_code() {
let code1 = Rc::new(super::Code {
value: std::cell::RefCell::new("echo hello".to_string()),
start_line_number: std::num::NonZero::new(1).unwrap(),
source: Rc::new(super::Source::CommandString),
});
let code2 = Rc::new(super::Code {
value: std::cell::RefCell::new("ls -l".to_string()),
start_line_number: std::num::NonZero::new(1).unwrap(),
source: Rc::new(super::Source::CommandString),
});
let span = Span {
range: 0..2,
role: SpanRole::Primary {
label: "list".into(),
},
};
let mut snippets = vec![Snippet::with_code(&code1)];
add_span(&code2, span, &mut snippets);
assert_eq!(snippets.len(), 2);
assert_eq!(snippets[0].code.value.borrow().as_str(), "echo hello");
assert_eq!(snippets[0].spans.len(), 0);
assert_eq!(snippets[1].code.value.borrow().as_str(), "ls -l");
assert_eq!(snippets[1].spans.len(), 1);
assert_eq!(snippets[1].spans[0].range, 0..2);
assert_eq!(
snippets[1].spans[0].role,
SpanRole::Primary {
label: "list".into()
}
);
}
impl super::Source {
pub fn extend_with_context<'a>(&'a self, snippets: &mut Vec<Snippet<'a>>) {
use super::Source::*;
match self {
Unknown
| Stdin
| CommandString
| CommandFile { .. }
| VariableValue { .. }
| InitFile { .. }
| Other { .. } => (),
CommandSubst { original } => {
let range = original.byte_range();
let role = SpanRole::Supplementary {
label: "command substitution appeared here".into(),
};
add_span(&original.code, Span { range, role }, snippets);
}
Arith { original } => {
let range = original.byte_range();
let role = SpanRole::Supplementary {
label: "arithmetic expansion appeared here".into(),
};
add_span(&original.code, Span { range, role }, snippets);
}
Eval { original } => {
let range = original.byte_range();
let role = SpanRole::Supplementary {
label: "command passed to the eval built-in here".into(),
};
add_span(&original.code, Span { range, role }, snippets);
}
DotScript { name, origin } => {
let range = origin.byte_range();
let role = SpanRole::Supplementary {
label: format!("script `{name}` was sourced here").into(),
};
add_span(&origin.code, Span { range, role }, snippets);
}
Trap { origin, .. } => {
let range = origin.byte_range();
let role = SpanRole::Supplementary {
label: "trap was set here".into(),
};
add_span(&origin.code, Span { range, role }, snippets);
}
Alias { original, alias } => {
let range = original.byte_range();
let role = SpanRole::Supplementary {
label: format!("alias `{}` was substituted here", alias.name).into(),
};
add_span(&original.code, Span { range, role }, snippets);
original.code.source.extend_with_context(snippets);
let range = alias.origin.byte_range();
let role = SpanRole::Supplementary {
label: format!("alias `{}` was defined here", alias.name).into(),
};
add_span(&alias.origin.code, Span { range, role }, snippets);
alias.origin.code.source.extend_with_context(snippets);
}
}
}
}
mod annotate_snippets_support {
use super::*;
impl From<ReportType> for annotate_snippets::Level<'_> {
fn from(r#type: ReportType) -> Self {
use ReportType::*;
match r#type {
None => Self::INFO.no_name(),
Error => Self::ERROR,
Warning => Self::WARNING,
}
}
}
fn span_to_annotation<'a>(span: &'a Span<'a>) -> annotate_snippets::Annotation<'a> {
use annotate_snippets::AnnotationKind as AK;
let (kind, label) = match &span.role {
SpanRole::Primary { label } => (AK::Primary, label),
SpanRole::Supplementary { label } => (AK::Context, label),
};
kind.span(span.range.clone()).label(label)
}
fn snippet_to_annotation_snippet<'a>(
snippet: &'a Snippet<'a>,
) -> annotate_snippets::Snippet<'a, annotate_snippets::Annotation<'a>> {
annotate_snippets::Snippet::source(snippet.code_string())
.line_start(
snippet
.code
.start_line_number
.get()
.try_into()
.unwrap_or(usize::MAX),
)
.path(snippet.code.source.label())
.annotations(snippet.spans.iter().map(span_to_annotation))
}
impl From<FootnoteType> for annotate_snippets::Level<'_> {
fn from(r#type: FootnoteType) -> Self {
use FootnoteType::*;
match r#type {
None => Self::INFO.no_name(),
Note => Self::NOTE,
Suggestion => Self::HELP,
}
}
}
impl<'a> From<Footnote<'a>> for annotate_snippets::Message<'a> {
fn from(footer: Footnote<'a>) -> Self {
annotate_snippets::Level::from(footer.r#type).message(footer.label)
}
}
impl<'a> From<&'a Footnote<'a>> for annotate_snippets::Message<'a> {
fn from(footer: &'a Footnote<'a>) -> Self {
annotate_snippets::Level::from(footer.r#type).message(&*footer.label)
}
}
impl<'a> From<&'a Report<'a>> for annotate_snippets::Group<'a> {
fn from(report: &'a Report<'a>) -> Self {
let title = annotate_snippets::Level::from(report.r#type).primary_title(&*report.title);
let title = if let Some(id) = &report.id {
title.id(&**id)
} else {
title
};
title
.elements(report.snippets.iter().map(snippet_to_annotation_snippet))
.elements(
report
.footnotes
.iter()
.map(annotate_snippets::Message::from),
)
}
}
}