From 37542efce234d5dd9c192ead275f7944a8872773 Mon Sep 17 00:00:00 2001 From: Scott Schafer Date: Tue, 24 Jun 2025 03:30:20 -0600 Subject: [PATCH] fix: Take Into instead of &str --- src/level.rs | 23 ++++----- src/renderer/mod.rs | 56 +++++++++++----------- src/renderer/source_map.rs | 11 +++-- src/snippet.rs | 96 ++++++++++++++++++++++++++++---------- tests/formatter.rs | 2 +- 5 files changed, 119 insertions(+), 69 deletions(-) diff --git a/src/level.rs b/src/level.rs index d3db1814..5934a280 100644 --- a/src/level.rs +++ b/src/level.rs @@ -2,8 +2,9 @@ use crate::renderer::stylesheet::Stylesheet; use crate::snippet::{ERROR_TXT, HELP_TXT, INFO_TXT, NOTE_TXT, WARNING_TXT}; -use crate::Title; +use crate::{OptionCow, Title}; use anstyle::Style; +use std::borrow::Cow; /// Default `error:` [`Level`] pub const ERROR: Level<'_> = Level { @@ -38,7 +39,7 @@ pub const HELP: Level<'_> = Level { /// [`Title`] severity level #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Level<'a> { - pub(crate) name: Option>, + pub(crate) name: Option>>, pub(crate) level: LevelInner, } @@ -56,9 +57,9 @@ impl<'a> Level<'a> { /// not allowed to be passed to this function. /// /// - pub fn text(self, text: Option<&'a str>) -> Level<'a> { + pub fn text(self, text: impl Into>) -> Level<'a> { Level { - name: Some(text), + name: Some(text.into().0), level: self.level, } } @@ -72,11 +73,11 @@ impl<'a> Level<'a> { /// not allowed to be passed to this function. /// /// - pub fn title(self, title: &'a str) -> Title<'a> { + pub fn title(self, title: impl Into>) -> Title<'a> { Title { level: self, id: None, - title, + title: title.into(), is_pre_styled: false, } } @@ -89,18 +90,18 @@ impl<'a> Level<'a> { /// used to normalize untrusted text before it is passed to this function. /// /// - pub fn pre_styled_title(self, title: &'a str) -> Title<'a> { + pub fn pre_styled_title(self, title: impl Into>) -> Title<'a> { Title { level: self, id: None, - title, + title: title.into(), is_pre_styled: true, } } - pub(crate) fn as_str(&self) -> &'a str { - match (self.name, self.level) { - (Some(Some(name)), _) => name, + pub(crate) fn as_str(&'a self) -> &'a str { + match (&self.name, self.level) { + (Some(Some(name)), _) => name.as_ref(), (Some(None), _) => "", (None, LevelInner::Error) => ERROR_TXT, (None, LevelInner::Warning) => WARNING_TXT, diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 12139c38..0cc43d26 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -229,14 +229,14 @@ impl Renderer { .find_map(|s| match &s { Element::Cause(cause) => { if cause.markers.iter().any(|m| m.kind.is_primary()) { - Some(cause.path) + Some(cause.path.as_ref()) } else { None } } Element::Origin(origin) => { if origin.primary { - Some(Some(origin.path)) + Some(Some(&origin.path)) } else { None } @@ -248,8 +248,8 @@ impl Renderer { .elements .iter() .find_map(|s| match &s { - Element::Cause(cause) => Some(cause.path), - Element::Origin(origin) => Some(Some(origin.path)), + Element::Cause(cause) => Some(cause.path.as_ref()), + Element::Origin(origin) => Some(Some(&origin.path)), _ => None, }) .unwrap_or_default(), @@ -269,7 +269,7 @@ impl Renderer { let mut max_depth = 0; for e in &group.elements { if let Element::Cause(cause) = e { - let source_map = SourceMap::new(cause.source, cause.line_start); + let source_map = SourceMap::new(&cause.source, cause.line_start); let (depth, annotated_lines) = source_map.annotated_lines(cause.markers.clone(), cause.fold); max_depth = max(max_depth, depth); @@ -340,7 +340,7 @@ impl Renderer { } Element::Suggestion(suggestion) => { let source_map = - SourceMap::new(suggestion.source, suggestion.line_start); + SourceMap::new(&suggestion.source, suggestion.line_start); self.emit_suggestion_default( &mut buffer, suggestion, @@ -426,10 +426,10 @@ impl Renderer { let labels_inner = cause .markers .iter() - .filter_map(|ann| match ann.label { + .filter_map(|ann| match &ann.label { Some(msg) if ann.kind.is_primary() => { if !msg.trim().is_empty() { - Some(msg.to_owned()) + Some(msg.to_string()) } else { None } @@ -442,11 +442,11 @@ impl Renderer { labels = Some(labels_inner); } - if let Some(path) = cause.path { - let mut origin = Origin::new(path); + if let Some(path) = &cause.path { + let mut origin = Origin::new(path.as_ref()); origin.primary = true; - let source_map = SourceMap::new(cause.source, cause.line_start); + let source_map = SourceMap::new(&cause.source, cause.line_start); let (_depth, annotated_lines) = source_map.annotated_lines(cause.markers.clone(), cause.fold); @@ -531,7 +531,7 @@ impl Renderer { if title.level.name != Some(None) { buffer.append(buffer_msg_line_offset, title.level.as_str(), label_style); label_width += title.level.as_str().len(); - if let Some(Id { id: Some(id), url }) = title.id { + if let Some(Id { id: Some(id), url }) = &title.id { buffer.append(buffer_msg_line_offset, "[", label_style); if let Some(url) = url.as_ref() { buffer.append( @@ -575,9 +575,9 @@ impl Renderer { }); let (title_str, style) = if title.is_pre_styled { - (title.title.to_owned(), ElementStyle::NoStyle) + (title.title.to_string(), ElementStyle::NoStyle) } else { - (normalize_whitespace(title.title), title_element_style) + (normalize_whitespace(&title.title), title_element_style) }; for (i, text) in title_str.lines().enumerate() { if i != 0 { @@ -654,7 +654,7 @@ impl Renderer { format!("{}:{}:{}", origin.path, line, col) } (Some(line), None) => format!("{}:{}", origin.path, line), - _ => origin.path.to_owned(), + _ => origin.path.to_string(), }; buffer.append(buffer_msg_line_offset, &str, ElementStyle::LineAndColumn); @@ -671,17 +671,17 @@ impl Renderer { buffer: &mut StyledBuffer, max_line_num_len: usize, snippet: &Snippet<'_, Annotation<'_>>, - primary_path: Option<&str>, + primary_path: Option<&Cow<'_, str>>, sm: &SourceMap<'_>, annotated_lines: &[AnnotatedLineInfo<'_>], multiline_depth: usize, is_cont: bool, ) { - if let Some(path) = snippet.path { - let mut origin = Origin::new(path); + if let Some(path) = &snippet.path { + let mut origin = Origin::new(path.as_ref()); // print out the span location and spacer before we print the annotated source // to do this, we need to know if this span will be primary - let is_primary = primary_path == Some(origin.path); + let is_primary = primary_path == Some(&origin.path); if is_primary { origin.primary = true; @@ -1394,7 +1394,7 @@ impl Renderer { } else { (pos + 2, annotation.start.display.saturating_sub(left)) }; - if let Some(label) = annotation.label { + if let Some(label) = &annotation.label { buffer.puts(line_offset + pos, code_offset + col, label, style); } } @@ -1535,7 +1535,7 @@ impl Renderer { suggestion: &Snippet<'_, Patch<'_>>, max_line_num_len: usize, sm: &SourceMap<'_>, - primary_path: Option<&str>, + primary_path: Option<&Cow<'_, str>>, is_cont: bool, ) { let suggestions = sm.splice_lines(suggestion.markers.clone()); @@ -1558,8 +1558,8 @@ impl Renderer { ElementStyle::LineNumber, ); } - if suggestion.path != primary_path { - if let Some(path) = suggestion.path { + if suggestion.path.as_ref() != primary_path { + if let Some(path) = suggestion.path.as_ref() { let (loc, _) = sm.span_to_locations(parts[0].span.clone()); // --> file.rs:line:col // | @@ -1781,7 +1781,7 @@ impl Renderer { // ...or trailing spaces. Account for substitutions containing unicode // characters. let sub_len: usize = str_width(if is_whitespace_addition { - part.replacement + &part.replacement } else { part.replacement.trim() }); @@ -1802,7 +1802,7 @@ impl Renderer { let padding: usize = max_line_num_len + 3; for p in underline_start..underline_end { if matches!(show_code_change, DisplaySuggestion::Underline) - && is_different(sm, part.replacement, part.span.clone()) + && is_different(sm, &part.replacement, part.span.clone()) { // If this is a replacement, underline with `~`, if this is an addition // underline with `+`. @@ -1903,7 +1903,7 @@ impl Renderer { } // length of the code after substitution - let full_sub_len = str_width(part.replacement) as isize; + let full_sub_len = str_width(&part.replacement) as isize; // length of the code to be substituted let snippet_len = span_end_pos as isize - span_start_pos as isize; @@ -2631,7 +2631,7 @@ pub(crate) struct LineAnnotation<'a> { pub kind: AnnotationKind, /// Optional label to display adjacent to the annotation. - pub label: Option<&'a str>, + pub label: Option>, /// Is this a single line, multiline or multiline span minimized down to a /// smaller span. @@ -2658,7 +2658,7 @@ impl LineAnnotation<'_> { } pub(crate) fn has_label(&self) -> bool { - if let Some(label) = self.label { + if let Some(label) = &self.label { // Consider labels with no text as effectively not being there // to avoid weird output with unnecessary vertical lines, like: // diff --git a/src/renderer/source_map.rs b/src/renderer/source_map.rs index 026b2a42..e2e4b61c 100644 --- a/src/renderer/source_map.rs +++ b/src/renderer/source_map.rs @@ -1,5 +1,6 @@ use crate::renderer::{char_width, is_different, num_overlap, LineAnnotation, LineAnnotationType}; use crate::{Annotation, AnnotationKind, Patch}; +use std::borrow::Cow; use std::cmp::{max, min}; use std::ops::Range; @@ -453,14 +454,14 @@ impl<'a> SourceMap<'a> { .replacement .split('\n') .next() - .unwrap_or(part.replacement) + .unwrap_or(&part.replacement) .chars() .map(|c| match c { '\t' => 4, _ => 1, }) .sum(); - if !is_different(self, part.replacement, part.span.clone()) { + if !is_different(self, &part.replacement, part.span.clone()) { // Account for cases where we are suggesting the same code that's already // there. This shouldn't happen often, but in some cases for multipart // suggestions it's much easier to handle it here than in the origin. @@ -470,7 +471,7 @@ impl<'a> SourceMap<'a> { end: (cur_lo.char as isize + acc + len) as usize, }); } - buf.push_str(part.replacement); + buf.push_str(&part.replacement); // Account for the difference between the width of the current code and the // snippet being suggested, so that the *later* suggestions are correctly // aligned on the screen. Note that cur_hi and cur_lo can be on different @@ -514,7 +515,7 @@ pub(crate) struct MultilineAnnotation<'a> { pub start: Loc, pub end: Loc, pub kind: AnnotationKind, - pub label: Option<&'a str>, + pub label: Option>, pub overlaps_exactly: bool, pub highlight_source: bool, } @@ -555,7 +556,7 @@ impl<'a> MultilineAnnotation<'a> { }, end: self.end, kind: self.kind, - label: self.label, + label: self.label.clone(), annotation_type: LineAnnotationType::MultilineEnd(self.depth), highlight_source: self.highlight_source, } diff --git a/src/snippet.rs b/src/snippet.rs index 6e9a78c7..f8b243fb 100644 --- a/src/snippet.rs +++ b/src/snippet.rs @@ -2,6 +2,7 @@ use crate::renderer::source_map::SourceMap; use crate::Level; +use std::borrow::Cow; use std::ops::Range; pub(crate) const ERROR_TXT: &str = "error"; @@ -12,8 +13,8 @@ pub(crate) const WARNING_TXT: &str = "warning"; #[derive(Clone, Debug, Default)] pub(crate) struct Id<'a> { - pub(crate) id: Option<&'a str>, - pub(crate) url: Option<&'a str>, + pub(crate) id: Option>, + pub(crate) url: Option>, } /// An [`Element`] container @@ -100,7 +101,7 @@ pub struct Padding; pub struct Title<'a> { pub(crate) level: Level<'a>, pub(crate) id: Option>, - pub(crate) title: &'a str, + pub(crate) title: Cow<'a, str>, pub(crate) is_pre_styled: bool, } @@ -117,8 +118,8 @@ impl<'a> Title<'a> { /// not allowed to be passed to this function. /// /// - pub fn id(mut self, id: &'a str) -> Self { - self.id.get_or_insert(Id::default()).id = Some(id); + pub fn id(mut self, id: impl Into>) -> Self { + self.id.get_or_insert(Id::default()).id = Some(id.into()); self } @@ -128,8 +129,8 @@ impl<'a> Title<'a> { /// `id` present /// /// - pub fn id_url(mut self, url: &'a str) -> Self { - self.id.get_or_insert(Id::default()).url = Some(url); + pub fn id_url(mut self, url: impl Into>) -> Self { + self.id.get_or_insert(Id::default()).url = Some(url.into()); self } } @@ -139,9 +140,9 @@ impl<'a> Title<'a> { /// If you do not have [source][Snippet::source] available, see instead [`Origin`] #[derive(Clone, Debug)] pub struct Snippet<'a, T> { - pub(crate) path: Option<&'a str>, + pub(crate) path: Option>, pub(crate) line_start: usize, - pub(crate) source: &'a str, + pub(crate) source: Cow<'a, str>, pub(crate) markers: Vec, pub(crate) fold: bool, } @@ -156,11 +157,11 @@ impl<'a, T: Clone> Snippet<'a, T> { /// not allowed to be passed to this function. /// /// - pub fn source(source: &'a str) -> Self { + pub fn source(source: impl Into>) -> Self { Self { path: None, line_start: 1, - source, + source: source.into(), markers: vec![], fold: false, } @@ -182,8 +183,8 @@ impl<'a, T: Clone> Snippet<'a, T> { /// not allowed to be passed to this function. /// /// - pub fn path(mut self, path: &'a str) -> Self { - self.path = Some(path); + pub fn path(mut self, path: impl Into>) -> Self { + self.path = path.into().0; self } @@ -228,7 +229,7 @@ impl<'a> Snippet<'a, Patch<'a>> { #[derive(Clone, Debug)] pub struct Annotation<'a> { pub(crate) span: Range, - pub(crate) label: Option<&'a str>, + pub(crate) label: Option>, pub(crate) kind: AnnotationKind, pub(crate) highlight_source: bool, } @@ -245,8 +246,8 @@ impl<'a> Annotation<'a> { /// not allowed to be passed to this function. /// /// - pub fn label(mut self, label: &'a str) -> Self { - self.label = Some(label); + pub fn label(mut self, label: impl Into>) -> Self { + self.label = label.into().0; self } @@ -286,7 +287,7 @@ impl AnnotationKind { #[derive(Clone, Debug)] pub struct Patch<'a> { pub(crate) span: Range, - pub(crate) replacement: &'a str, + pub(crate) replacement: Cow<'a, str>, } impl<'a> Patch<'a> { @@ -299,8 +300,11 @@ impl<'a> Patch<'a> { /// not allowed to be passed to this function. /// /// - pub fn new(span: Range, replacement: &'a str) -> Self { - Self { span, replacement } + pub fn new(span: Range, replacement: impl Into>) -> Self { + Self { + span, + replacement: replacement.into(), + } } pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool { @@ -344,9 +348,9 @@ impl<'a> Patch<'a> { return; }; - if let Some((prefix, substr, suffix)) = as_substr(snippet, self.replacement) { + if let Some((prefix, substr, suffix)) = as_substr(snippet, &self.replacement) { self.span = self.span.start + prefix..self.span.end.saturating_sub(suffix); - self.replacement = substr; + self.replacement = Cow::Owned(substr.to_owned()); } } } @@ -356,7 +360,7 @@ impl<'a> Patch<'a> { /// If you have source available, see instead [`Snippet`] #[derive(Clone, Debug)] pub struct Origin<'a> { - pub(crate) path: &'a str, + pub(crate) path: Cow<'a, str>, pub(crate) line: Option, pub(crate) char_column: Option, pub(crate) primary: bool, @@ -370,9 +374,9 @@ impl<'a> Origin<'a> { /// not allowed to be passed to this function. /// /// - pub fn new(path: &'a str) -> Self { + pub fn new(path: impl Into>) -> Self { Self { - path, + path: path.into(), line: None, char_column: None, primary: false, @@ -407,6 +411,50 @@ impl<'a> Origin<'a> { } } +impl<'a> From> for Origin<'a> { + fn from(origin: Cow<'a, str>) -> Self { + Self::new(origin) + } +} + +#[derive(Debug)] +pub struct OptionCow<'a>(pub(crate) Option>); + +impl<'a, T: Into>> From> for OptionCow<'a> { + fn from(value: Option) -> Self { + Self(value.map(Into::into)) + } +} + +impl<'a> From<&'a Cow<'a, str>> for OptionCow<'a> { + fn from(value: &'a Cow<'a, str>) -> Self { + Self(Some(Cow::Borrowed(value))) + } +} + +impl<'a> From> for OptionCow<'a> { + fn from(value: Cow<'a, str>) -> Self { + Self(Some(value)) + } +} + +impl<'a> From<&'a str> for OptionCow<'a> { + fn from(value: &'a str) -> Self { + Self(Some(Cow::Borrowed(value))) + } +} +impl<'a> From for OptionCow<'a> { + fn from(value: String) -> Self { + Self(Some(Cow::Owned(value))) + } +} + +impl<'a> From<&'a String> for OptionCow<'a> { + fn from(value: &'a String) -> Self { + Self(Some(Cow::Borrowed(value.as_str()))) + } +} + /// Given an original string like `AACC`, and a suggestion like `AABBCC`, try to detect /// the case where a substring of the suggestion is "sandwiched" in the original, like /// `BB` is. Return the length of the prefix, the "trimmed" suggestion, and the length diff --git a/tests/formatter.rs b/tests/formatter.rs index 34c40bf8..c538a9e8 100644 --- a/tests/formatter.rs +++ b/tests/formatter.rs @@ -2448,7 +2448,7 @@ fn secondary_title_no_level_text() { ) .element( Level::NOTE - .text(None) + .text(None::<&str>) .title("expected reference `&str`\nfound reference `&'static [u8; 0]`"), )];