Skip to content

chore: Don't allow Title in element #260

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/highlight_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ fn main() {}
),
)
.element(
Level::NOTE.title("The runtime heap is not yet available at compile-time, so no runtime heap allocations can be created."),
Level::NOTE.message("The runtime heap is not yet available at compile-time, so no runtime heap allocations can be created."),

)];

Expand Down
92 changes: 49 additions & 43 deletions src/renderer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,32 +285,45 @@ impl Renderer {
source_map_annotated_lines.push_back((source_map, annotated_lines));
}
}
let mut message_iter = group.elements.iter().enumerate().peekable();
let mut message_iter = group.elements.iter().peekable();
let mut last_was_suggestion = false;
let mut first_was_title = false;
while let Some((i, section)) = message_iter.next() {
let peek = message_iter.peek().map(|(_, s)| s).copied();
if let Some(title) = &group.title {
let peek = message_iter.peek().copied();
let title_style = if g == 0 {
TitleStyle::MainHeader
} else {
TitleStyle::Header
};
let buffer_msg_line_offset = buffer.num_lines();
self.render_title(
&mut buffer,
title,
max_line_num_len,
title_style,
matches!(peek, Some(Element::Message(_))),
buffer_msg_line_offset,
);
let buffer_msg_line_offset = buffer.num_lines();

if matches!(peek, Some(Element::Message(_))) {
self.draw_col_separator_no_space(
&mut buffer,
buffer_msg_line_offset,
max_line_num_len + 1,
);
}
if peek.is_none() && g == 0 && group_len > 1 {
self.draw_col_separator_end(
&mut buffer,
buffer_msg_line_offset,
max_line_num_len + 1,
);
}
}
let mut seen_primary = false;
while let Some(section) = message_iter.next() {
let peek = message_iter.peek().copied();
match &section {
Element::Title(title) => {
if i == 0 {
first_was_title = true;
}
let title_style = match (i == 0, g == 0) {
(true, true) => TitleStyle::MainHeader,
(true, false) => TitleStyle::Header,
(false, _) => TitleStyle::Secondary,
};
let buffer_msg_line_offset = buffer.num_lines();
self.render_title(
&mut buffer,
title,
max_line_num_len,
title_style,
matches!(peek, Some(Element::Title(_) | Element::Message(_))),
buffer_msg_line_offset,
);
last_was_suggestion = false;
}
Element::Message(title) => {
let title_style = TitleStyle::Secondary;
let buffer_msg_line_offset = buffer.num_lines();
Expand All @@ -321,8 +334,7 @@ impl Renderer {
title_style,
matches!(
peek,
Some(Element::Title(_) | Element::Message(_))
| Some(Element::Padding(_))
Some(Element::Message(_)) | Some(Element::Padding(_))
),
buffer_msg_line_offset,
);
Expand All @@ -332,8 +344,9 @@ impl Renderer {
if let Some((source_map, annotated_lines)) =
source_map_annotated_lines.pop_front()
{
let is_primary = primary_path == cause.path.as_ref()
&& i == first_was_title as usize;
let is_primary =
primary_path == cause.path.as_ref() && !seen_primary;
seen_primary |= is_primary;
self.render_snippet_annotations(
&mut buffer,
max_line_num_len,
Expand All @@ -348,7 +361,7 @@ impl Renderer {
if g == 0 {
let current_line = buffer.num_lines();
match peek {
Some(Element::Message(_) | Element::Title(_)) => {
Some(Element::Message(_)) => {
self.draw_col_separator_no_space(
&mut buffer,
current_line,
Expand Down Expand Up @@ -389,6 +402,8 @@ impl Renderer {

Element::Origin(origin) => {
let buffer_msg_line_offset = buffer.num_lines();
let is_primary = primary_path == Some(&origin.path) && !seen_primary;
seen_primary |= is_primary;
self.render_origin(
&mut buffer,
max_line_num_len,
Expand All @@ -414,18 +429,15 @@ impl Renderer {
}
}
}
if g == 0
&& (matches!(section, Element::Origin(_))
|| (matches!(section, Element::Title(_)) && i == 0))
{
if g == 0 && matches!(section, Element::Origin(_)) {
let current_line = buffer.num_lines();
if peek.is_none() && group_len > 1 {
self.draw_col_separator_end(
&mut buffer,
current_line,
max_line_num_len + 1,
);
} else if matches!(peek, Some(Element::Message(_) | Element::Title(_))) {
} else if matches!(peek, Some(Element::Message(_))) {
self.draw_col_separator_no_space(
&mut buffer,
current_line,
Expand All @@ -452,11 +464,8 @@ impl Renderer {
let mut labels = None;
let group = groups.first().expect("Expected at least one group");

let Some(Element::Title(title)) = group.elements.first() else {
panic!(
"Expected first element to be a Title, got: {:?}",
group.elements.first()
);
let Some(title) = &group.title else {
panic!("Expected a Title");
};

if let Some(Element::Cause(cause)) = group
Expand Down Expand Up @@ -2952,10 +2961,7 @@ fn max_line_number(groups: &[Group<'_>]) -> usize {
v.elements
.iter()
.map(|s| match s {
Element::Title(_)
| Element::Message(_)
| Element::Origin(_)
| Element::Padding(_) => 0,
Element::Message(_) | Element::Origin(_) | Element::Padding(_) => 0,
Element::Cause(cause) => {
if cause.fold {
let end = cause
Expand Down
15 changes: 6 additions & 9 deletions src/snippet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,17 @@ pub(crate) struct Id<'a> {
#[derive(Clone, Debug)]
pub struct Group<'a> {
pub(crate) primary_level: Level<'a>,
pub(crate) title: Option<Title<'a>>,
pub(crate) elements: Vec<Element<'a>>,
}

impl<'a> Group<'a> {
/// Create group with a title, deriving the primary [`Level`] for [`Annotation`]s from it
pub fn with_title(title: Title<'a>) -> Self {
let level = title.level.clone();
Self::with_level(level).element(title)
let mut x = Self::with_level(level);
x.title = Some(title);
x
}

/// Create a title-less group with a primary [`Level`] for [`Annotation`]s
Expand All @@ -55,6 +58,7 @@ impl<'a> Group<'a> {
pub fn with_level(level: Level<'a>) -> Self {
Self {
primary_level: level,
title: None,
elements: vec![],
}
}
Expand All @@ -70,28 +74,21 @@ impl<'a> Group<'a> {
}

pub fn is_empty(&self) -> bool {
self.elements.is_empty()
self.elements.is_empty() && self.title.is_none()
}
}

/// A section of content within a [`Group`]
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum Element<'a> {
Title(Title<'a>),
Message(Message<'a>),
Cause(Snippet<'a, Annotation<'a>>),
Suggestion(Snippet<'a, Patch<'a>>),
Origin(Origin<'a>),
Padding(Padding),
}

impl<'a> From<Title<'a>> for Element<'a> {
fn from(value: Title<'a>) -> Self {
Element::Title(value)
}
}

impl<'a> From<Message<'a>> for Element<'a> {
fn from(value: Message<'a>) -> Self {
Element::Message(value)
Expand Down
4 changes: 2 additions & 2 deletions tests/color/multiline_removal_suggestion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,10 @@ fn main() {}
)
.element(
Level::HELP
.title("the trait `Iterator` is not implemented for `(bool, HashSet<u8>)`"),
.message("the trait `Iterator` is not implemented for `(bool, HashSet<u8>)`"),
)
.element(
Level::NOTE.title("required for `(bool, HashSet<u8>)` to implement `IntoIterator`"),
Level::NOTE.message("required for `(bool, HashSet<u8>)` to implement `IntoIterator`"),
),
Group::with_title(Level::NOTE.title("required by a bound in `flatten`"))
.element(
Expand Down
20 changes: 10 additions & 10 deletions tests/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1666,7 +1666,7 @@ fn main() {
.annotation(AnnotationKind::Primary.span(89..90))
).element(
Level::NOTE
.title("required for the cast from `Box<Result<Result<(), Result<Result<(), Result<Result<(), Option<{integer}>>, ()>>, ()>>, ()>>` to `Box<(dyn Future<Error = Foo> + 'static)>`")
.message("required for the cast from `Box<Result<Result<(), Result<Result<(), Result<Result<(), Option<{integer}>>, ()>>, ()>>, ()>>` to `Box<(dyn Future<Error = Foo> + 'static)>`")
,
)];

Expand Down Expand Up @@ -1752,9 +1752,9 @@ fn main() {
.annotation(AnnotationKind::Primary.span(89..90))
).element(
Level::NOTE
.title("required for the cast from `Box<Result<Result<(), Result<Result<(), Result<Result<(), Option<{integer}>>, ()>>, ()>>, ()>>` to `Box<(dyn Future<Error = Foo> + 'static)>`")
.message("required for the cast from `Box<Result<Result<(), Result<Result<(), Result<Result<(), Option<{integer}>>, ()>>, ()>>, ()>>` to `Box<(dyn Future<Error = Foo> + 'static)>`")
).element(
Level::NOTE.title("a second note"),
Level::NOTE.message("a second note"),
)];

let expected = str![[r#"
Expand Down Expand Up @@ -1902,13 +1902,13 @@ fn main() {
)
).element(
Level::NOTE
.title("expected struct `Atype<Btype<..., i32>, i32>`\n found enum `Result<Result<..., _>, _>`")
.message("expected struct `Atype<Btype<..., i32>, i32>`\n found enum `Result<Result<..., _>, _>`")
).element(
Level::NOTE
.title("the full name for the type has been written to '$TEST_BUILD_DIR/$FILE.long-type-hash.txt'")
.message("the full name for the type has been written to '$TEST_BUILD_DIR/$FILE.long-type-hash.txt'")
).element(
Level::NOTE
.title("consider using `--verbose` to print the full type name to the console")
.message("consider using `--verbose` to print the full type name to the console")
,
)];

Expand Down Expand Up @@ -1986,7 +1986,7 @@ fn main() {
),
).element(
Level::NOTE
.title("expected fn pointer `for<'a> fn(Box<(dyn Any + Send + 'a)>) -> Pin<_>`\n found fn item `fn(Box<(dyn Any + Send + 'static)>) -> Pin<_> {wrapped_fn}`")
.message("expected fn pointer `for<'a> fn(Box<(dyn Any + Send + 'a)>) -> Pin<_>`\n found fn item `fn(Box<(dyn Any + Send + 'static)>) -> Pin<_> {wrapped_fn}`")
,
),Group::with_title(
Level::NOTE.title("function defined here"),
Expand Down Expand Up @@ -2077,7 +2077,7 @@ fn unicode_cut_handling2() {
.fold(false)
.annotation(AnnotationKind::Primary.span(499..500).label("expected item"))
).element(
Level::NOTE.title("for a full list of items that can appear in modules, see <https://doc.rust-lang.org/reference/items.html>")
Level::NOTE.message("for a full list of items that can appear in modules, see <https://doc.rust-lang.org/reference/items.html>")

)];

Expand Down Expand Up @@ -2114,7 +2114,7 @@ fn unicode_cut_handling3() {
.fold(false)
.annotation(AnnotationKind::Primary.span(251..254).label("expected item"))
).element(
Level::NOTE.title("for a full list of items that can appear in modules, see <https://doc.rust-lang.org/reference/items.html>")
Level::NOTE.message("for a full list of items that can appear in modules, see <https://doc.rust-lang.org/reference/items.html>")

)];

Expand Down Expand Up @@ -2151,7 +2151,7 @@ fn unicode_cut_handling4() {
.fold(false)
.annotation(AnnotationKind::Primary.span(334..335).label("expected item"))
).element(
Level::NOTE.title("for a full list of items that can appear in modules, see <https://doc.rust-lang.org/reference/items.html>")
Level::NOTE.message("for a full list of items that can appear in modules, see <https://doc.rust-lang.org/reference/items.html>")

)];

Expand Down
15 changes: 8 additions & 7 deletions tests/rustc_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1365,11 +1365,11 @@ outer_macro!(FirstStruct, FirstAttrStruct);
)
.element(
Level::HELP
.title("remove the `#[macro_export]` or move this `macro_rules!` outside the of the current function `main`")
.message("remove the `#[macro_export]` or move this `macro_rules!` outside the of the current function `main`")
)
.element(
Level::NOTE
.title("a `macro_rules!` definition is non-local if it is nested inside an item and has a `#[macro_export]` attribute")
.message("a `macro_rules!` definition is non-local if it is nested inside an item and has a `#[macro_export]` attribute")
),
Group::with_title(Level::NOTE.title("the lint level is defined here"))
.element(
Expand Down Expand Up @@ -1965,7 +1965,7 @@ fn main() {
.annotation(AnnotationKind::Primary.span(267..380)),
)
.element(
Level::NOTE.title("`Iterator::map`, like many of the methods on `Iterator`, gets executed lazily, meaning that its effects won't be visible until it is iterated")),
Level::NOTE.message("`Iterator::map`, like many of the methods on `Iterator`, gets executed lazily, meaning that its effects won't be visible until it is iterated")),
Group::with_title(Level::HELP.title("you might have meant to use `Iterator::for_each`"))
.element(
Snippet::source(source)
Expand Down Expand Up @@ -2555,7 +2555,8 @@ fn mismatched_types1() {
),
)
.element(
Level::NOTE.title("expected reference `&[u8]`\n found reference `&'static str`"),
Level::NOTE
.message("expected reference `&[u8]`\n found reference `&'static str`"),
),
];

Expand Down Expand Up @@ -2607,7 +2608,7 @@ fn mismatched_types2() {
)
.element(
Level::NOTE
.title("expected reference `&str`\n found reference `&'static [u8; 0]`"),
.message("expected reference `&str`\n found reference `&'static [u8; 0]`"),
),
];

Expand Down Expand Up @@ -2734,7 +2735,7 @@ pub struct Foo; //~^ ERROR
.annotation(AnnotationKind::Primary.span(111..126)),
)
.element(
Level::NOTE.title("bare URLs are not automatically turned into clickable links"),
Level::NOTE.message("bare URLs are not automatically turned into clickable links"),
),
Group::with_title(Level::NOTE.title("the lint level is defined here")).element(
Snippet::source(source_0)
Expand Down Expand Up @@ -3427,7 +3428,7 @@ fn main() {
.label("associated type `Pr` not found for this struct"),
),
)
.element(Level::NOTE.title("the associated type was found for\n"))];
.element(Level::NOTE.message("the associated type was found for\n"))];

let expected = str![[r#"
error[E0220]: associated type `Pr` not found for `S<bool>` in the current scope
Expand Down
Loading