From 9735524533967ae13d5d58f9feafd601037375fa Mon Sep 17 00:00:00 2001 From: Jonathan Brouwer Date: Wed, 22 Oct 2025 21:43:50 +0200 Subject: [PATCH 1/8] Add `AcceptContext::suggestions` Signed-off-by: Jonathan Brouwer --- .../rustc_attr_parsing/src/attributes/cfg.rs | 5 ++-- .../src/attributes/inline.rs | 3 +- .../src/attributes/link_attrs.rs | 3 +- .../src/attributes/macro_attrs.rs | 19 ++++-------- .../src/attributes/must_use.rs | 3 +- .../src/attributes/test_attrs.rs | 6 ++-- compiler/rustc_attr_parsing/src/context.rs | 30 +++++++++++-------- .../src/session_diagnostics.rs | 9 +++--- compiler/rustc_feature/src/builtin_attrs.rs | 21 ++++++++----- ...invalid_macro_export_argument.allow.stderr | 8 ++--- .../invalid_macro_export_argument.deny.stderr | 16 +++++----- tests/ui/attributes/malformed-attrs.stderr | 2 +- 12 files changed, 61 insertions(+), 64 deletions(-) diff --git a/compiler/rustc_attr_parsing/src/attributes/cfg.rs b/compiler/rustc_attr_parsing/src/attributes/cfg.rs index af94e8acaf68e..9f0c98668924a 100644 --- a/compiler/rustc_attr_parsing/src/attributes/cfg.rs +++ b/compiler/rustc_attr_parsing/src/attributes/cfg.rs @@ -326,7 +326,8 @@ pub fn parse_cfg_attr( }) { Ok(r) => return Some(r), Err(e) => { - let suggestions = CFG_ATTR_TEMPLATE.suggestions(cfg_attr.style, sym::cfg_attr); + let suggestions = + CFG_ATTR_TEMPLATE.suggestions(Some(cfg_attr.style), sym::cfg_attr); e.with_span_suggestions( cfg_attr.span, "must be of the form", @@ -356,7 +357,7 @@ pub fn parse_cfg_attr( template: CFG_ATTR_TEMPLATE, attribute: AttrPath::from_ast(&cfg_attr.get_normal_item().path), reason, - attr_style: cfg_attr.style, + suggestions: CFG_ATTR_TEMPLATE.suggestions(Some(cfg_attr.style), sym::cfg_attr), }); } } diff --git a/compiler/rustc_attr_parsing/src/attributes/inline.rs b/compiler/rustc_attr_parsing/src/attributes/inline.rs index a10ad27fcc68b..eda272fb7f2b7 100644 --- a/compiler/rustc_attr_parsing/src/attributes/inline.rs +++ b/compiler/rustc_attr_parsing/src/attributes/inline.rs @@ -56,8 +56,7 @@ impl SingleAttributeParser for InlineParser { } } ArgParser::NameValue(_) => { - let suggestions = >::TEMPLATE - .suggestions(cx.attr_style, "inline"); + let suggestions = cx.suggestions(); let span = cx.attr_span; cx.emit_lint(AttributeLintKind::IllFormedAttributeInput { suggestions }, span); return None; diff --git a/compiler/rustc_attr_parsing/src/attributes/link_attrs.rs b/compiler/rustc_attr_parsing/src/attributes/link_attrs.rs index 40ecd91e5cfc1..797d04b914dd5 100644 --- a/compiler/rustc_attr_parsing/src/attributes/link_attrs.rs +++ b/compiler/rustc_attr_parsing/src/attributes/link_attrs.rs @@ -71,8 +71,7 @@ impl CombineAttributeParser for LinkParser { // Specifically `#[link = "dl"]` is accepted with a FCW // For more information, see https://github.com/rust-lang/rust/pull/143193 ArgParser::NameValue(nv) if nv.value_as_str().is_some_and(|v| v == sym::dl) => { - let suggestions = >::TEMPLATE - .suggestions(cx.attr_style, "link"); + let suggestions = cx.suggestions(); let span = cx.attr_span; cx.emit_lint(AttributeLintKind::IllFormedAttributeInput { suggestions }, span); return None; diff --git a/compiler/rustc_attr_parsing/src/attributes/macro_attrs.rs b/compiler/rustc_attr_parsing/src/attributes/macro_attrs.rs index 849141c34f7d9..787003519e78b 100644 --- a/compiler/rustc_attr_parsing/src/attributes/macro_attrs.rs +++ b/compiler/rustc_attr_parsing/src/attributes/macro_attrs.rs @@ -1,4 +1,3 @@ -use rustc_ast::AttrStyle; use rustc_errors::DiagArgValue; use rustc_hir::attrs::MacroUseArgs; @@ -102,7 +101,7 @@ impl AttributeParser for MacroUseParser { } } ArgParser::NameValue(_) => { - let suggestions = MACRO_USE_TEMPLATE.suggestions(cx.attr_style, sym::macro_use); + let suggestions = cx.suggestions(); cx.emit_err(IllFormedAttributeInputLint { num_suggestions: suggestions.len(), suggestions: DiagArgValue::StrListSepByAnd( @@ -149,19 +148,14 @@ impl SingleAttributeParser for MacroExportParser { ]); fn convert(cx: &mut AcceptContext<'_, '_, S>, args: &ArgParser<'_>) -> Option { - let suggestions = || { - >::TEMPLATE - .suggestions(AttrStyle::Inner, "macro_export") - }; let local_inner_macros = match args { ArgParser::NoArgs => false, ArgParser::List(list) => { let Some(l) = list.single() else { let span = cx.attr_span; + let suggestions = cx.suggestions(); cx.emit_lint( - AttributeLintKind::InvalidMacroExportArguments { - suggestions: suggestions(), - }, + AttributeLintKind::InvalidMacroExportArguments { suggestions }, span, ); return None; @@ -170,10 +164,9 @@ impl SingleAttributeParser for MacroExportParser { Some(sym::local_inner_macros) => true, _ => { let span = cx.attr_span; + let suggestions = cx.suggestions(); cx.emit_lint( - AttributeLintKind::InvalidMacroExportArguments { - suggestions: suggestions(), - }, + AttributeLintKind::InvalidMacroExportArguments { suggestions }, span, ); return None; @@ -182,7 +175,7 @@ impl SingleAttributeParser for MacroExportParser { } ArgParser::NameValue(_) => { let span = cx.attr_span; - let suggestions = suggestions(); + let suggestions = cx.suggestions(); cx.emit_err(IllFormedAttributeInputLint { num_suggestions: suggestions.len(), suggestions: DiagArgValue::StrListSepByAnd( diff --git a/compiler/rustc_attr_parsing/src/attributes/must_use.rs b/compiler/rustc_attr_parsing/src/attributes/must_use.rs index e6a5141d78307..51b43e96adf99 100644 --- a/compiler/rustc_attr_parsing/src/attributes/must_use.rs +++ b/compiler/rustc_attr_parsing/src/attributes/must_use.rs @@ -45,8 +45,7 @@ impl SingleAttributeParser for MustUseParser { Some(value_str) } ArgParser::List(_) => { - let suggestions = >::TEMPLATE - .suggestions(cx.attr_style, "must_use"); + let suggestions = cx.suggestions(); cx.emit_err(IllFormedAttributeInputLint { num_suggestions: suggestions.len(), suggestions: DiagArgValue::StrListSepByAnd( diff --git a/compiler/rustc_attr_parsing/src/attributes/test_attrs.rs b/compiler/rustc_attr_parsing/src/attributes/test_attrs.rs index 510ff1ded4904..23ecc0bf7d29f 100644 --- a/compiler/rustc_attr_parsing/src/attributes/test_attrs.rs +++ b/compiler/rustc_attr_parsing/src/attributes/test_attrs.rs @@ -20,8 +20,7 @@ impl SingleAttributeParser for IgnoreParser { ArgParser::NoArgs => None, ArgParser::NameValue(name_value) => { let Some(str_value) = name_value.value_as_str() else { - let suggestions = >::TEMPLATE - .suggestions(cx.attr_style, "ignore"); + let suggestions = cx.suggestions(); let span = cx.attr_span; cx.emit_lint( AttributeLintKind::IllFormedAttributeInput { suggestions }, @@ -32,8 +31,7 @@ impl SingleAttributeParser for IgnoreParser { Some(str_value) } ArgParser::List(_) => { - let suggestions = >::TEMPLATE - .suggestions(cx.attr_style, "ignore"); + let suggestions = cx.suggestions(); let span = cx.attr_span; cx.emit_lint(AttributeLintKind::IllFormedAttributeInput { suggestions }, span); return None; diff --git a/compiler/rustc_attr_parsing/src/context.rs b/compiler/rustc_attr_parsing/src/context.rs index 6749bdfb1a672..7b14e24a218b7 100644 --- a/compiler/rustc_attr_parsing/src/context.rs +++ b/compiler/rustc_attr_parsing/src/context.rs @@ -427,7 +427,7 @@ impl<'f, 'sess: 'f, S: Stage> AcceptContext<'f, 'sess, S> { i.kind.is_bytestr().then(|| self.sess().source_map().start_point(i.span)) }), }, - attr_style: self.attr_style, + suggestions: self.suggestions(), }) } @@ -438,7 +438,7 @@ impl<'f, 'sess: 'f, S: Stage> AcceptContext<'f, 'sess, S> { template: self.template.clone(), attribute: self.attr_path.clone(), reason: AttributeParseErrorReason::ExpectedIntegerLiteral, - attr_style: self.attr_style, + suggestions: self.suggestions(), }) } @@ -449,7 +449,7 @@ impl<'f, 'sess: 'f, S: Stage> AcceptContext<'f, 'sess, S> { template: self.template.clone(), attribute: self.attr_path.clone(), reason: AttributeParseErrorReason::ExpectedList, - attr_style: self.attr_style, + suggestions: self.suggestions(), }) } @@ -460,7 +460,7 @@ impl<'f, 'sess: 'f, S: Stage> AcceptContext<'f, 'sess, S> { template: self.template.clone(), attribute: self.attr_path.clone(), reason: AttributeParseErrorReason::ExpectedNoArgs, - attr_style: self.attr_style, + suggestions: self.suggestions(), }) } @@ -472,7 +472,7 @@ impl<'f, 'sess: 'f, S: Stage> AcceptContext<'f, 'sess, S> { template: self.template.clone(), attribute: self.attr_path.clone(), reason: AttributeParseErrorReason::ExpectedIdentifier, - attr_style: self.attr_style, + suggestions: self.suggestions(), }) } @@ -485,7 +485,7 @@ impl<'f, 'sess: 'f, S: Stage> AcceptContext<'f, 'sess, S> { template: self.template.clone(), attribute: self.attr_path.clone(), reason: AttributeParseErrorReason::ExpectedNameValue(name), - attr_style: self.attr_style, + suggestions: self.suggestions(), }) } @@ -497,7 +497,7 @@ impl<'f, 'sess: 'f, S: Stage> AcceptContext<'f, 'sess, S> { template: self.template.clone(), attribute: self.attr_path.clone(), reason: AttributeParseErrorReason::DuplicateKey(key), - attr_style: self.attr_style, + suggestions: self.suggestions(), }) } @@ -510,7 +510,7 @@ impl<'f, 'sess: 'f, S: Stage> AcceptContext<'f, 'sess, S> { template: self.template.clone(), attribute: self.attr_path.clone(), reason: AttributeParseErrorReason::UnexpectedLiteral, - attr_style: self.attr_style, + suggestions: self.suggestions(), }) } @@ -521,7 +521,7 @@ impl<'f, 'sess: 'f, S: Stage> AcceptContext<'f, 'sess, S> { template: self.template.clone(), attribute: self.attr_path.clone(), reason: AttributeParseErrorReason::ExpectedSingleArgument, - attr_style: self.attr_style, + suggestions: self.suggestions(), }) } @@ -532,7 +532,7 @@ impl<'f, 'sess: 'f, S: Stage> AcceptContext<'f, 'sess, S> { template: self.template.clone(), attribute: self.attr_path.clone(), reason: AttributeParseErrorReason::ExpectedAtLeastOneArgument, - attr_style: self.attr_style, + suggestions: self.suggestions(), }) } @@ -552,7 +552,7 @@ impl<'f, 'sess: 'f, S: Stage> AcceptContext<'f, 'sess, S> { strings: false, list: false, }, - attr_style: self.attr_style, + suggestions: self.suggestions(), }) } @@ -573,7 +573,7 @@ impl<'f, 'sess: 'f, S: Stage> AcceptContext<'f, 'sess, S> { strings: false, list: true, }, - attr_style: self.attr_style, + suggestions: self.suggestions(), }) } @@ -593,7 +593,7 @@ impl<'f, 'sess: 'f, S: Stage> AcceptContext<'f, 'sess, S> { strings: true, list: false, }, - attr_style: self.attr_style, + suggestions: self.suggestions(), }) } @@ -605,6 +605,10 @@ impl<'f, 'sess: 'f, S: Stage> AcceptContext<'f, 'sess, S> { span, ); } + + pub(crate) fn suggestions(&self) -> Vec { + self.template.suggestions(Some(self.attr_style), &self.attr_path) + } } impl<'f, 'sess, S: Stage> Deref for AcceptContext<'f, 'sess, S> { diff --git a/compiler/rustc_attr_parsing/src/session_diagnostics.rs b/compiler/rustc_attr_parsing/src/session_diagnostics.rs index 7b82f3baec093..82bd29218313e 100644 --- a/compiler/rustc_attr_parsing/src/session_diagnostics.rs +++ b/compiler/rustc_attr_parsing/src/session_diagnostics.rs @@ -1,6 +1,6 @@ use std::num::IntErrorKind; -use rustc_ast::{self as ast, AttrStyle, Path}; +use rustc_ast::{self as ast, Path}; use rustc_errors::codes::*; use rustc_errors::{ Applicability, Diag, DiagArgValue, DiagCtxtHandle, Diagnostic, EmissionGuarantee, Level, @@ -613,10 +613,10 @@ pub(crate) enum AttributeParseErrorReason<'a> { pub(crate) struct AttributeParseError<'a> { pub(crate) span: Span, pub(crate) attr_span: Span, - pub(crate) attr_style: AttrStyle, pub(crate) template: AttributeTemplate, pub(crate) attribute: AttrPath, pub(crate) reason: AttributeParseErrorReason<'a>, + pub(crate) suggestions: Vec, } impl<'a, G: EmissionGuarantee> Diagnostic<'a, G> for AttributeParseError<'_> { @@ -752,16 +752,15 @@ impl<'a, G: EmissionGuarantee> Diagnostic<'a, G> for AttributeParseError<'_> { if let Some(link) = self.template.docs { diag.note(format!("for more information, visit <{link}>")); } - let suggestions = self.template.suggestions(self.attr_style, &name); diag.span_suggestions( self.attr_span, - if suggestions.len() == 1 { + if self.suggestions.len() == 1 { "must be of the form" } else { "try changing it to one of the following valid forms of the attribute" }, - suggestions, + self.suggestions, Applicability::HasPlaceholders, ); diff --git a/compiler/rustc_feature/src/builtin_attrs.rs b/compiler/rustc_feature/src/builtin_attrs.rs index 220388ffe4353..a2acac8b3045d 100644 --- a/compiler/rustc_feature/src/builtin_attrs.rs +++ b/compiler/rustc_feature/src/builtin_attrs.rs @@ -133,24 +133,29 @@ pub struct AttributeTemplate { } impl AttributeTemplate { - pub fn suggestions(&self, style: AttrStyle, name: impl std::fmt::Display) -> Vec { + pub fn suggestions( + &self, + style: Option, + name: impl std::fmt::Display, + ) -> Vec { let mut suggestions = vec![]; - let inner = match style { - AttrStyle::Outer => "", - AttrStyle::Inner => "!", + let (start, end) = match style { + Some(AttrStyle::Outer) => ("#[", "]"), + Some(AttrStyle::Inner) => ("#![", "]"), + None => ("", ""), }; if self.word { - suggestions.push(format!("#{inner}[{name}]")); + suggestions.push(format!("{start}{name}{end}")); } if let Some(descr) = self.list { for descr in descr { - suggestions.push(format!("#{inner}[{name}({descr})]")); + suggestions.push(format!("{start}{name}({descr}){end}")); } } - suggestions.extend(self.one_of.iter().map(|&word| format!("#{inner}[{name}({word})]"))); + suggestions.extend(self.one_of.iter().map(|&word| format!("{start}{name}({word}){end}"))); if let Some(descr) = self.name_value_str { for descr in descr { - suggestions.push(format!("#{inner}[{name} = \"{descr}\"]")); + suggestions.push(format!("{start}{name} = \"{descr}\"{end}")); } } suggestions.sort(); diff --git a/tests/ui/attributes/invalid_macro_export_argument.allow.stderr b/tests/ui/attributes/invalid_macro_export_argument.allow.stderr index 162b315b072a4..2db60a6897282 100644 --- a/tests/ui/attributes/invalid_macro_export_argument.allow.stderr +++ b/tests/ui/attributes/invalid_macro_export_argument.allow.stderr @@ -1,5 +1,5 @@ Future incompatibility report: Future breakage diagnostic: -warning: valid forms for the attribute are `#![macro_export(local_inner_macros)]` and `#![macro_export]` +warning: valid forms for the attribute are `#[macro_export(local_inner_macros)]` and `#[macro_export]` --> $DIR/invalid_macro_export_argument.rs:7:1 | LL | #[macro_export(hello, world)] @@ -9,7 +9,7 @@ LL | #[macro_export(hello, world)] = note: for more information, see issue #57571 Future breakage diagnostic: -warning: valid forms for the attribute are `#![macro_export(local_inner_macros)]` and `#![macro_export]` +warning: valid forms for the attribute are `#[macro_export(local_inner_macros)]` and `#[macro_export]` --> $DIR/invalid_macro_export_argument.rs:14:1 | LL | #[macro_export(not_local_inner_macros)] @@ -19,7 +19,7 @@ LL | #[macro_export(not_local_inner_macros)] = note: for more information, see issue #57571 Future breakage diagnostic: -warning: valid forms for the attribute are `#![macro_export(local_inner_macros)]` and `#![macro_export]` +warning: valid forms for the attribute are `#[macro_export(local_inner_macros)]` and `#[macro_export]` --> $DIR/invalid_macro_export_argument.rs:31:1 | LL | #[macro_export()] @@ -29,7 +29,7 @@ LL | #[macro_export()] = note: for more information, see issue #57571 Future breakage diagnostic: -warning: valid forms for the attribute are `#![macro_export(local_inner_macros)]` and `#![macro_export]` +warning: valid forms for the attribute are `#[macro_export(local_inner_macros)]` and `#[macro_export]` --> $DIR/invalid_macro_export_argument.rs:38:1 | LL | #[macro_export("blah")] diff --git a/tests/ui/attributes/invalid_macro_export_argument.deny.stderr b/tests/ui/attributes/invalid_macro_export_argument.deny.stderr index ad225ae187b11..fd50b824d0a26 100644 --- a/tests/ui/attributes/invalid_macro_export_argument.deny.stderr +++ b/tests/ui/attributes/invalid_macro_export_argument.deny.stderr @@ -1,4 +1,4 @@ -error: valid forms for the attribute are `#![macro_export(local_inner_macros)]` and `#![macro_export]` +error: valid forms for the attribute are `#[macro_export(local_inner_macros)]` and `#[macro_export]` --> $DIR/invalid_macro_export_argument.rs:7:1 | LL | #[macro_export(hello, world)] @@ -12,7 +12,7 @@ note: the lint level is defined here LL | #![cfg_attr(deny, deny(invalid_macro_export_arguments))] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -error: valid forms for the attribute are `#![macro_export(local_inner_macros)]` and `#![macro_export]` +error: valid forms for the attribute are `#[macro_export(local_inner_macros)]` and `#[macro_export]` --> $DIR/invalid_macro_export_argument.rs:14:1 | LL | #[macro_export(not_local_inner_macros)] @@ -21,7 +21,7 @@ LL | #[macro_export(not_local_inner_macros)] = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release! = note: for more information, see issue #57571 -error: valid forms for the attribute are `#![macro_export(local_inner_macros)]` and `#![macro_export]` +error: valid forms for the attribute are `#[macro_export(local_inner_macros)]` and `#[macro_export]` --> $DIR/invalid_macro_export_argument.rs:31:1 | LL | #[macro_export()] @@ -30,7 +30,7 @@ LL | #[macro_export()] = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release! = note: for more information, see issue #57571 -error: valid forms for the attribute are `#![macro_export(local_inner_macros)]` and `#![macro_export]` +error: valid forms for the attribute are `#[macro_export(local_inner_macros)]` and `#[macro_export]` --> $DIR/invalid_macro_export_argument.rs:38:1 | LL | #[macro_export("blah")] @@ -42,7 +42,7 @@ LL | #[macro_export("blah")] error: aborting due to 4 previous errors Future incompatibility report: Future breakage diagnostic: -error: valid forms for the attribute are `#![macro_export(local_inner_macros)]` and `#![macro_export]` +error: valid forms for the attribute are `#[macro_export(local_inner_macros)]` and `#[macro_export]` --> $DIR/invalid_macro_export_argument.rs:7:1 | LL | #[macro_export(hello, world)] @@ -57,7 +57,7 @@ LL | #![cfg_attr(deny, deny(invalid_macro_export_arguments))] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Future breakage diagnostic: -error: valid forms for the attribute are `#![macro_export(local_inner_macros)]` and `#![macro_export]` +error: valid forms for the attribute are `#[macro_export(local_inner_macros)]` and `#[macro_export]` --> $DIR/invalid_macro_export_argument.rs:14:1 | LL | #[macro_export(not_local_inner_macros)] @@ -72,7 +72,7 @@ LL | #![cfg_attr(deny, deny(invalid_macro_export_arguments))] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Future breakage diagnostic: -error: valid forms for the attribute are `#![macro_export(local_inner_macros)]` and `#![macro_export]` +error: valid forms for the attribute are `#[macro_export(local_inner_macros)]` and `#[macro_export]` --> $DIR/invalid_macro_export_argument.rs:31:1 | LL | #[macro_export()] @@ -87,7 +87,7 @@ LL | #![cfg_attr(deny, deny(invalid_macro_export_arguments))] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Future breakage diagnostic: -error: valid forms for the attribute are `#![macro_export(local_inner_macros)]` and `#![macro_export]` +error: valid forms for the attribute are `#[macro_export(local_inner_macros)]` and `#[macro_export]` --> $DIR/invalid_macro_export_argument.rs:38:1 | LL | #[macro_export("blah")] diff --git a/tests/ui/attributes/malformed-attrs.stderr b/tests/ui/attributes/malformed-attrs.stderr index c29bd0245bf04..5627cb452a815 100644 --- a/tests/ui/attributes/malformed-attrs.stderr +++ b/tests/ui/attributes/malformed-attrs.stderr @@ -701,7 +701,7 @@ error: valid forms for the attribute are `#[macro_use(name1, name2, ...)]` and ` LL | #[macro_use = 1] | ^^^^^^^^^^^^^^^^ -error: valid forms for the attribute are `#![macro_export(local_inner_macros)]` and `#![macro_export]` +error: valid forms for the attribute are `#[macro_export(local_inner_macros)]` and `#[macro_export]` --> $DIR/malformed-attrs.rs:221:1 | LL | #[macro_export = 18] From 24502dd2d2e6e02289308411e12c6cf034cae666 Mon Sep 17 00:00:00 2001 From: Jonathan Brouwer Date: Wed, 22 Oct 2025 21:57:13 +0200 Subject: [PATCH 2/8] Improve suggestions for `cfg_attr` Signed-off-by: Jonathan Brouwer --- compiler/rustc_attr_parsing/src/attributes/cfg.rs | 1 + compiler/rustc_attr_parsing/src/context.rs | 15 +++++++++++++-- compiler/rustc_attr_parsing/src/interface.rs | 4 ++++ .../cfg_attr-attr-syntax-validation.stderr | 14 ++++++-------- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/compiler/rustc_attr_parsing/src/attributes/cfg.rs b/compiler/rustc_attr_parsing/src/attributes/cfg.rs index 9f0c98668924a..dab9e7666c23a 100644 --- a/compiler/rustc_attr_parsing/src/attributes/cfg.rs +++ b/compiler/rustc_attr_parsing/src/attributes/cfg.rs @@ -389,6 +389,7 @@ fn parse_cfg_attr_internal<'a>( let cfg_predicate = AttributeParser::parse_single_args( sess, attribute.span, + attribute.get_normal_item().span(), attribute.style, AttrPath { segments: attribute diff --git a/compiler/rustc_attr_parsing/src/context.rs b/compiler/rustc_attr_parsing/src/context.rs index 7b14e24a218b7..6694dac8bb25a 100644 --- a/compiler/rustc_attr_parsing/src/context.rs +++ b/compiler/rustc_attr_parsing/src/context.rs @@ -337,8 +337,16 @@ pub struct Late; /// Gives [`AttributeParser`]s enough information to create errors, for example. pub struct AcceptContext<'f, 'sess, S: Stage> { pub(crate) shared: SharedContext<'f, 'sess, S>, - /// The span of the attribute currently being parsed + + /// The outer span of the attribute currently being parsed + /// #[attribute(...)] + /// ^^^^^^^^^^^^^^^^^ outer span + /// For attributes in `cfg_attr`, the outer span and inner spans are equal. pub(crate) attr_span: Span, + /// The inner span of the attribute currently being parsed + /// #[attribute(...)] + /// ^^^^^^^^^^^^^^ inner span + pub(crate) inner_span: Span, /// Whether it is an inner or outer attribute pub(crate) attr_style: AttrStyle, @@ -607,7 +615,10 @@ impl<'f, 'sess: 'f, S: Stage> AcceptContext<'f, 'sess, S> { } pub(crate) fn suggestions(&self) -> Vec { - self.template.suggestions(Some(self.attr_style), &self.attr_path) + // If the outer and inner spans are equal, we are parsing an attribute from `cfg_attr`, + // So don't display an attribute style in the suggestions + let style = (self.attr_span != self.inner_span).then_some(self.attr_style); + self.template.suggestions(style, &self.attr_path) } } diff --git a/compiler/rustc_attr_parsing/src/interface.rs b/compiler/rustc_attr_parsing/src/interface.rs index b8ef11c26d80e..953b0ebfaf04f 100644 --- a/compiler/rustc_attr_parsing/src/interface.rs +++ b/compiler/rustc_attr_parsing/src/interface.rs @@ -142,6 +142,7 @@ impl<'sess> AttributeParser<'sess, Early> { Self::parse_single_args( sess, attr.span, + normal_attr.item.span(), attr.style, path.get_attribute_path(), target_span, @@ -159,6 +160,7 @@ impl<'sess> AttributeParser<'sess, Early> { pub fn parse_single_args( sess: &'sess Session, attr_span: Span, + inner_span: Span, attr_style: AttrStyle, attr_path: AttrPath, target_span: Span, @@ -186,6 +188,7 @@ impl<'sess> AttributeParser<'sess, Early> { }, }, attr_span, + inner_span, attr_style, template, attr_path, @@ -305,6 +308,7 @@ impl<'sess, S: Stage> AttributeParser<'sess, S> { emit_lint: &mut emit_lint, }, attr_span: lower_span(attr.span), + inner_span: lower_span(attr.get_normal_item().span()), attr_style: attr.style, template: &accept.template, attr_path: path.get_attribute_path(), diff --git a/tests/ui/conditional-compilation/cfg_attr-attr-syntax-validation.stderr b/tests/ui/conditional-compilation/cfg_attr-attr-syntax-validation.stderr index 2b7a7da3e33da..fd03fa62864af 100644 --- a/tests/ui/conditional-compilation/cfg_attr-attr-syntax-validation.stderr +++ b/tests/ui/conditional-compilation/cfg_attr-attr-syntax-validation.stderr @@ -113,7 +113,7 @@ error[E0539]: malformed `link_section` attribute input --> $DIR/cfg_attr-attr-syntax-validation.rs:44:18 | LL | #[cfg_attr(true, link_section)] - | ^^^^^^^^^^^^ help: must be of the form: `#[link_section = "name"]` + | ^^^^^^^^^^^^ help: must be of the form: `link_section = "name"` | = note: for more information, visit @@ -129,14 +129,12 @@ LL | #[cfg_attr(true, inline())] help: try changing it to one of the following valid forms of the attribute | LL - #[cfg_attr(true, inline())] -LL + #[cfg_attr(true, #[inline(always)])] - | -LL - #[cfg_attr(true, inline())] -LL + #[cfg_attr(true, #[inline(never)])] - | -LL - #[cfg_attr(true, inline())] -LL + #[cfg_attr(true, #[inline])] +LL + #[cfg_attr(true, inline)] | +LL | #[cfg_attr(true, inline(always))] + | ++++++ +LL | #[cfg_attr(true, inline(never))] + | +++++ warning: `#[link_section]` attribute cannot be used on structs --> $DIR/cfg_attr-attr-syntax-validation.rs:44:18 From b67453fccd7919af0e59b2134e6b3a5e26e76378 Mon Sep 17 00:00:00 2001 From: Camille GILLOT Date: Sat, 21 Jun 2025 14:44:59 +0000 Subject: [PATCH 3/8] Stop passing resolver disambiguator state to AST lowering. --- compiler/rustc_ast_lowering/src/expr.rs | 9 ++++++++- compiler/rustc_ast_lowering/src/lib.rs | 15 +++++++++++++-- compiler/rustc_ast_lowering/src/pat.rs | 4 +++- compiler/rustc_hir/src/definitions.rs | 13 ++++++++++++- compiler/rustc_middle/src/ty/mod.rs | 3 --- compiler/rustc_middle/src/ty/print/pretty.rs | 1 + compiler/rustc_resolve/src/lib.rs | 1 - .../src/cfi/typeid/itanium_cxx_abi/encode.rs | 4 +++- compiler/rustc_symbol_mangling/src/v0.rs | 4 +++- ...emit-type-metadata-id-itanium-cxx-abi-paths.rs | 6 +++--- 10 files changed, 46 insertions(+), 14 deletions(-) diff --git a/compiler/rustc_ast_lowering/src/expr.rs b/compiler/rustc_ast_lowering/src/expr.rs index 4a9b9f544b53d..c443b1e3a03c5 100644 --- a/compiler/rustc_ast_lowering/src/expr.rs +++ b/compiler/rustc_ast_lowering/src/expr.rs @@ -7,6 +7,7 @@ use rustc_data_structures::stack::ensure_sufficient_stack; use rustc_hir as hir; use rustc_hir::attrs::AttributeKind; use rustc_hir::def::{DefKind, Res}; +use rustc_hir::definitions::DefPathData; use rustc_hir::{HirId, Target, find_attr}; use rustc_middle::span_bug; use rustc_middle::ty::TyCtxt; @@ -461,7 +462,13 @@ impl<'hir> LoweringContext<'_, 'hir> { for (idx, arg) in args.iter().cloned().enumerate() { if legacy_args_idx.contains(&idx) { let node_id = self.next_node_id(); - self.create_def(node_id, None, DefKind::AnonConst, f.span); + self.create_def( + node_id, + None, + DefKind::AnonConst, + DefPathData::LateAnonConst, + f.span, + ); let mut visitor = WillCreateDefIdsVisitor {}; let const_value = if let ControlFlow::Break(span) = visitor.visit_expr(&arg) { Box::new(Expr { diff --git a/compiler/rustc_ast_lowering/src/lib.rs b/compiler/rustc_ast_lowering/src/lib.rs index d959657e7fe57..be9db9257356c 100644 --- a/compiler/rustc_ast_lowering/src/lib.rs +++ b/compiler/rustc_ast_lowering/src/lib.rs @@ -51,6 +51,7 @@ use rustc_data_structures::tagged_ptr::TaggedRef; use rustc_errors::{DiagArgFromDisplay, DiagCtxtHandle}; use rustc_hir::def::{DefKind, LifetimeRes, Namespace, PartialRes, PerNS, Res}; use rustc_hir::def_id::{CRATE_DEF_ID, LOCAL_CRATE, LocalDefId}; +use rustc_hir::definitions::{DefPathData, DisambiguatorState}; use rustc_hir::lints::DelayedLint; use rustc_hir::{ self as hir, AngleBrackets, ConstArg, GenericArg, HirId, ItemLocalMap, LifetimeSource, @@ -93,6 +94,7 @@ rustc_fluent_macro::fluent_messages! { "../messages.ftl" } struct LoweringContext<'a, 'hir> { tcx: TyCtxt<'hir>, resolver: &'a mut ResolverAstLowering, + disambiguator: DisambiguatorState, /// Used to allocate HIR nodes. arena: &'hir hir::Arena<'hir>, @@ -155,6 +157,7 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> { // Pseudo-globals. tcx, resolver, + disambiguator: DisambiguatorState::new(), arena: tcx.hir_arena, // HirId handling. @@ -546,6 +549,7 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> { node_id: ast::NodeId, name: Option, def_kind: DefKind, + def_path_data: DefPathData, span: Span, ) -> LocalDefId { let parent = self.current_hir_id_owner.def_id; @@ -561,7 +565,7 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> { let def_id = self .tcx .at(span) - .create_def(parent, name, def_kind, None, &mut self.resolver.disambiguator) + .create_def(parent, name, def_kind, Some(def_path_data), &mut self.disambiguator) .def_id(); debug!("create_def: def_id_to_node_id[{:?}] <-> {:?}", def_id, node_id); @@ -846,6 +850,7 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> { param, Some(kw::UnderscoreLifetime), DefKind::LifetimeParam, + DefPathData::DesugaredAnonymousLifetime, ident.span, ); debug!(?_def_id); @@ -2290,7 +2295,13 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> { // We're lowering a const argument that was originally thought to be a type argument, // so the def collector didn't create the def ahead of time. That's why we have to do // it here. - let def_id = self.create_def(node_id, None, DefKind::AnonConst, span); + let def_id = self.create_def( + node_id, + None, + DefKind::AnonConst, + DefPathData::LateAnonConst, + span, + ); let hir_id = self.lower_node_id(node_id); let path_expr = Expr { diff --git a/compiler/rustc_ast_lowering/src/pat.rs b/compiler/rustc_ast_lowering/src/pat.rs index 7e4fa840f2523..815338c84fa6c 100644 --- a/compiler/rustc_ast_lowering/src/pat.rs +++ b/compiler/rustc_ast_lowering/src/pat.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use rustc_ast::*; use rustc_data_structures::stack::ensure_sufficient_stack; use rustc_hir::def::{DefKind, Res}; +use rustc_hir::definitions::DefPathData; use rustc_hir::{self as hir, LangItem, Target}; use rustc_middle::span_bug; use rustc_span::source_map::{Spanned, respan}; @@ -527,7 +528,8 @@ impl<'a, 'hir> LoweringContext<'a, 'hir> { // We're generating a range end that didn't exist in the AST, // so the def collector didn't create the def ahead of time. That's why we have to do // it here. - let def_id = self.create_def(node_id, None, DefKind::AnonConst, span); + let def_id = + self.create_def(node_id, None, DefKind::AnonConst, DefPathData::LateAnonConst, span); let hir_id = self.lower_node_id(node_id); let unstable_span = self.mark_span_with_reason( diff --git a/compiler/rustc_hir/src/definitions.rs b/compiler/rustc_hir/src/definitions.rs index 698406d53a436..07d5bcdd6ee92 100644 --- a/compiler/rustc_hir/src/definitions.rs +++ b/compiler/rustc_hir/src/definitions.rs @@ -302,6 +302,10 @@ pub enum DefPathData { Ctor, /// A constant expression (see `{ast,hir}::AnonConst`). AnonConst, + /// A constant expression created during AST->HIR lowering.. + LateAnonConst, + /// A fresh anonymous lifetime created by desugaring elided lifetimes. + DesugaredAnonymousLifetime, /// An existential `impl Trait` type node. /// Argument position `impl Trait` have a `TypeNs` with their pretty-printed name. OpaqueTy, @@ -454,6 +458,8 @@ impl DefPathData { TypeNs(name) | ValueNs(name) | MacroNs(name) | LifetimeNs(name) | OpaqueLifetime(name) => Some(name), + DesugaredAnonymousLifetime => Some(kw::UnderscoreLifetime), + Impl | ForeignMod | CrateRoot @@ -462,6 +468,7 @@ impl DefPathData { | Closure | Ctor | AnonConst + | LateAnonConst | OpaqueTy | AnonAssocTy(..) | SyntheticCoroutineBody @@ -475,6 +482,8 @@ impl DefPathData { TypeNs(name) | ValueNs(name) | MacroNs(name) | LifetimeNs(name) | AnonAssocTy(name) | OpaqueLifetime(name) => Some(name), + DesugaredAnonymousLifetime => Some(kw::UnderscoreLifetime), + Impl | ForeignMod | CrateRoot @@ -483,6 +492,7 @@ impl DefPathData { | Closure | Ctor | AnonConst + | LateAnonConst | OpaqueTy | SyntheticCoroutineBody | NestedStatic => None, @@ -502,7 +512,8 @@ impl DefPathData { GlobalAsm => DefPathDataName::Anon { namespace: sym::global_asm }, Closure => DefPathDataName::Anon { namespace: sym::closure }, Ctor => DefPathDataName::Anon { namespace: sym::constructor }, - AnonConst => DefPathDataName::Anon { namespace: sym::constant }, + AnonConst | LateAnonConst => DefPathDataName::Anon { namespace: sym::constant }, + DesugaredAnonymousLifetime => DefPathDataName::Named(kw::UnderscoreLifetime), OpaqueTy => DefPathDataName::Anon { namespace: sym::opaque }, AnonAssocTy(..) => DefPathDataName::Anon { namespace: sym::anon_assoc }, SyntheticCoroutineBody => DefPathDataName::Anon { namespace: sym::synthetic }, diff --git a/compiler/rustc_middle/src/ty/mod.rs b/compiler/rustc_middle/src/ty/mod.rs index c3e1defef809d..93d0c77c1daee 100644 --- a/compiler/rustc_middle/src/ty/mod.rs +++ b/compiler/rustc_middle/src/ty/mod.rs @@ -37,7 +37,6 @@ use rustc_errors::{Diag, ErrorGuaranteed, LintBuffer}; use rustc_hir::attrs::{AttributeKind, StrippedCfgItem}; use rustc_hir::def::{CtorKind, CtorOf, DefKind, DocLinkResMap, LifetimeRes, Res}; use rustc_hir::def_id::{CrateNum, DefId, DefIdMap, LocalDefId, LocalDefIdMap}; -use rustc_hir::definitions::DisambiguatorState; use rustc_hir::{LangItem, attrs as attr, find_attr}; use rustc_index::IndexVec; use rustc_index::bit_set::BitMatrix; @@ -211,8 +210,6 @@ pub struct ResolverAstLowering { pub node_id_to_def_id: NodeMap, - pub disambiguator: DisambiguatorState, - pub trait_map: NodeMap>, /// List functions and methods for which lifetime elision was successful. pub lifetime_elision_allowed: FxHashSet, diff --git a/compiler/rustc_middle/src/ty/print/pretty.rs b/compiler/rustc_middle/src/ty/print/pretty.rs index 0af7fe808ef91..06744ae6e2426 100644 --- a/compiler/rustc_middle/src/ty/print/pretty.rs +++ b/compiler/rustc_middle/src/ty/print/pretty.rs @@ -2156,6 +2156,7 @@ fn guess_def_namespace(tcx: TyCtxt<'_>, def_id: DefId) -> Namespace { DefPathData::ValueNs(..) | DefPathData::AnonConst + | DefPathData::LateAnonConst | DefPathData::Closure | DefPathData::Ctor => Namespace::ValueNS, diff --git a/compiler/rustc_resolve/src/lib.rs b/compiler/rustc_resolve/src/lib.rs index 1636605b234f4..f6a4f59cb339a 100644 --- a/compiler/rustc_resolve/src/lib.rs +++ b/compiler/rustc_resolve/src/lib.rs @@ -1791,7 +1791,6 @@ impl<'ra, 'tcx> Resolver<'ra, 'tcx> { .into_items() .map(|(k, f)| (k, f.key())) .collect(), - disambiguator: self.disambiguator, trait_map: self.trait_map, lifetime_elision_allowed: self.lifetime_elision_allowed, lint_buffer: Steal::new(self.lint_buffer), diff --git a/compiler/rustc_sanitizers/src/cfi/typeid/itanium_cxx_abi/encode.rs b/compiler/rustc_sanitizers/src/cfi/typeid/itanium_cxx_abi/encode.rs index 621cc0fb3ef16..60b3b42989b78 100644 --- a/compiler/rustc_sanitizers/src/cfi/typeid/itanium_cxx_abi/encode.rs +++ b/compiler/rustc_sanitizers/src/cfi/typeid/itanium_cxx_abi/encode.rs @@ -712,7 +712,8 @@ fn encode_ty_name(tcx: TyCtxt<'_>, def_id: DefId) -> String { hir::definitions::DefPathData::ValueNs(..) => "v", hir::definitions::DefPathData::Closure => "C", hir::definitions::DefPathData::Ctor => "c", - hir::definitions::DefPathData::AnonConst => "k", + hir::definitions::DefPathData::AnonConst => "K", + hir::definitions::DefPathData::LateAnonConst => "k", hir::definitions::DefPathData::OpaqueTy => "i", hir::definitions::DefPathData::SyntheticCoroutineBody => "s", hir::definitions::DefPathData::NestedStatic => "n", @@ -722,6 +723,7 @@ fn encode_ty_name(tcx: TyCtxt<'_>, def_id: DefId) -> String { | hir::definitions::DefPathData::MacroNs(..) | hir::definitions::DefPathData::OpaqueLifetime(..) | hir::definitions::DefPathData::LifetimeNs(..) + | hir::definitions::DefPathData::DesugaredAnonymousLifetime | hir::definitions::DefPathData::AnonAssocTy(..) => { bug!("encode_ty_name: unexpected `{:?}`", disambiguated_data.data); } diff --git a/compiler/rustc_symbol_mangling/src/v0.rs b/compiler/rustc_symbol_mangling/src/v0.rs index 2c71b22c4a2b9..e40fc04d76645 100644 --- a/compiler/rustc_symbol_mangling/src/v0.rs +++ b/compiler/rustc_symbol_mangling/src/v0.rs @@ -877,7 +877,8 @@ impl<'tcx> Printer<'tcx> for V0SymbolMangler<'tcx> { DefPathData::ValueNs(_) => 'v', DefPathData::Closure => 'C', DefPathData::Ctor => 'c', - DefPathData::AnonConst => 'k', + DefPathData::AnonConst => 'K', + DefPathData::LateAnonConst => 'k', DefPathData::OpaqueTy => 'i', DefPathData::SyntheticCoroutineBody => 's', DefPathData::NestedStatic => 'n', @@ -889,6 +890,7 @@ impl<'tcx> Printer<'tcx> for V0SymbolMangler<'tcx> { | DefPathData::Impl | DefPathData::MacroNs(_) | DefPathData::LifetimeNs(_) + | DefPathData::DesugaredAnonymousLifetime | DefPathData::OpaqueLifetime(_) | DefPathData::AnonAssocTy(..) => { bug!("symbol_names: unexpected DefPathData: {:?}", disambiguated_data.data) diff --git a/tests/codegen-llvm/sanitizer/cfi/emit-type-metadata-id-itanium-cxx-abi-paths.rs b/tests/codegen-llvm/sanitizer/cfi/emit-type-metadata-id-itanium-cxx-abi-paths.rs index f5846713bd531..4ce9c57070a72 100644 --- a/tests/codegen-llvm/sanitizer/cfi/emit-type-metadata-id-itanium-cxx-abi-paths.rs +++ b/tests/codegen-llvm/sanitizer/cfi/emit-type-metadata-id-itanium-cxx-abi-paths.rs @@ -78,9 +78,9 @@ pub fn foo12(_: &Type4, _: &Type4, _: &Type4) {} // CHECK: ![[TYPE4]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NtNCNvC{{[[:print:]]+}}_{{[[:print:]]+}}3foo11{{[{}][{}]}}closure{{[}][}]}}3FooEE"} // CHECK: ![[TYPE5]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NtNCNvC{{[[:print:]]+}}_{{[[:print:]]+}}3foo11{{[{}][{}]}}closure{{[}][}]}}3FooES0_E"} // CHECK: ![[TYPE6]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NtNCNvC{{[[:print:]]+}}_{{[[:print:]]+}}3foo11{{[{}][{}]}}closure{{[}][}]}}3FooES0_S0_E"} -// CHECK: ![[TYPE7]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NtNkNvC{{[[:print:]]+}}_{{[[:print:]]+}}3foo12{{[{}][{}]}}constant{{[}][}]}}3FooEE"} -// CHECK: ![[TYPE8]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NtNkNvC{{[[:print:]]+}}_{{[[:print:]]+}}3foo12{{[{}][{}]}}constant{{[}][}]}}3FooES0_E"} -// CHECK: ![[TYPE9]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NtNkNvC{{[[:print:]]+}}_{{[[:print:]]+}}3foo12{{[{}][{}]}}constant{{[}][}]}}3FooES0_S0_E"} +// CHECK: ![[TYPE7]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NtNKNvC{{[[:print:]]+}}_{{[[:print:]]+}}3foo12{{[{}][{}]}}constant{{[}][}]}}3FooEE"} +// CHECK: ![[TYPE8]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NtNKNvC{{[[:print:]]+}}_{{[[:print:]]+}}3foo12{{[{}][{}]}}constant{{[}][}]}}3FooES0_E"} +// CHECK: ![[TYPE9]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NtNKNvC{{[[:print:]]+}}_{{[[:print:]]+}}3foo12{{[{}][{}]}}constant{{[}][}]}}3FooES0_S0_E"} // CHECK: ![[TYPE10]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NvNINvC{{[[:print:]]+}}_{{[[:print:]]+}}3foo8{{[{}][{}]}}impl{{[}][}]}}3barEE"} // CHECK: ![[TYPE11]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NvNINvC{{[[:print:]]+}}_{{[[:print:]]+}}3foo8{{[{}][{}]}}impl{{[}][}]}}3barES0_E"} // CHECK: ![[TYPE12]] = !{i64 0, !"_ZTSFvu3refIu{{[0-9]+}}NvNINvC{{[[:print:]]+}}_{{[[:print:]]+}}3foo8{{[{}][{}]}}impl{{[}][}]}}3barES0_S0_E"} From 354b9779bf9b021c56f3ea14ed338e34377aa575 Mon Sep 17 00:00:00 2001 From: Scott Schafer Date: Sat, 29 Mar 2025 09:48:39 -0600 Subject: [PATCH 4/8] chore: Update to the latest annotate-snippets --- Cargo.lock | 34 +- compiler/rustc_errors/Cargo.toml | 2 +- .../src/annotate_snippet_emitter_writer.rs | 774 +++++++++++++++--- compiler/rustc_session/src/session.rs | 19 +- tests/ui/annotate-snippet/missing-type.stderr | 15 + .../ui/annotate-snippet/multiple-files.stderr | 7 +- tests/ui/annotate-snippet/multispan.stderr | 51 ++ 7 files changed, 780 insertions(+), 122 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c290b392d591..2320e33bc4bf1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,7 +75,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "710e8eae58854cdc1790fcb56cca04d712a17be849eeb81da2a724bf4bae2bc4" dependencies = [ "anstyle", - "unicode-width 0.2.1", + "unicode-width 0.2.2", +] + +[[package]] +name = "annotate-snippets" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47224528f74de27d1d06aad6a5dda4f865b6ebe2e56c538943d746a7270cb67e" +dependencies = [ + "anstyle", + "unicode-width 0.2.2", ] [[package]] @@ -136,7 +146,7 @@ dependencies = [ "anstyle-lossy", "anstyle-parse", "html-escape", - "unicode-width 0.2.1", + "unicode-width 0.2.2", ] [[package]] @@ -677,7 +687,7 @@ checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ "serde", "termcolor", - "unicode-width 0.2.1", + "unicode-width 0.2.2", ] [[package]] @@ -808,7 +818,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.1", + "unicode-width 0.2.2", "windows-sys 0.59.0", ] @@ -1485,7 +1495,7 @@ version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" dependencies = [ - "unicode-width 0.2.1", + "unicode-width 0.2.2", ] [[package]] @@ -1887,7 +1897,7 @@ dependencies = [ "console", "number_prefix", "portable-atomic", - "unicode-width 0.2.1", + "unicode-width 0.2.2", "web-time", ] @@ -3756,7 +3766,7 @@ dependencies = [ name = "rustc_errors" version = "0.0.0" dependencies = [ - "annotate-snippets 0.11.5", + "annotate-snippets 0.12.7", "anstream", "anstyle", "derive_setters", @@ -4331,7 +4341,7 @@ dependencies = [ "thin-vec", "tracing", "unicode-normalization", - "unicode-width 0.2.1", + "unicode-width 0.2.2", ] [[package]] @@ -4590,7 +4600,7 @@ dependencies = [ "sha1", "sha2", "tracing", - "unicode-width 0.2.1", + "unicode-width 0.2.2", ] [[package]] @@ -5936,9 +5946,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" @@ -6223,7 +6233,7 @@ dependencies = [ "bumpalo", "leb128fmt", "memchr", - "unicode-width 0.2.1", + "unicode-width 0.2.2", "wasm-encoder 0.240.0", ] diff --git a/compiler/rustc_errors/Cargo.toml b/compiler/rustc_errors/Cargo.toml index b7b5cbd35741f..6ade87ea3b255 100644 --- a/compiler/rustc_errors/Cargo.toml +++ b/compiler/rustc_errors/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] # tidy-alphabetical-start -annotate-snippets = "0.11" +annotate-snippets = "0.12.7" anstream = "0.6.20" anstyle = "1.0.13" derive_setters = "0.1.6" diff --git a/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs b/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs index 2eb3c23259ffa..854e3ddf15e4a 100644 --- a/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs +++ b/compiler/rustc_errors/src/annotate_snippet_emitter_writer.rs @@ -5,32 +5,70 @@ //! //! [annotate_snippets]: https://docs.rs/crate/annotate-snippets/ +use std::borrow::Cow; +use std::error::Report; +use std::fmt::Debug; +use std::io; +use std::io::Write; use std::sync::Arc; -use annotate_snippets::{Renderer, Snippet}; -use rustc_error_messages::FluentArgs; -use rustc_span::SourceFile; +use annotate_snippets::renderer::DEFAULT_TERM_WIDTH; +use annotate_snippets::{AnnotationKind, Group, Origin, Padding, Patch, Renderer, Snippet}; +use anstream::ColorChoice; +use derive_setters::Setters; +use rustc_data_structures::sync::IntoDynSyncSend; +use rustc_error_messages::{FluentArgs, SpanLabel}; +use rustc_lint_defs::pluralize; use rustc_span::source_map::SourceMap; +use rustc_span::{BytePos, FileName, Pos, SourceFile, Span}; +use tracing::debug; -use crate::emitter::FileWithAnnotatedLines; +use crate::emitter::{ + ConfusionType, Destination, MAX_SUGGESTIONS, OutputTheme, detect_confusion_type, is_different, + normalize_whitespace, should_show_source_code, +}; use crate::registry::Registry; -use crate::snippet::Line; use crate::translation::{Translator, to_fluent_args}; use crate::{ CodeSuggestion, DiagInner, DiagMessage, Emitter, ErrCode, Level, MultiSpan, Style, Subdiag, + SuggestionStyle, TerminalUrl, }; /// Generates diagnostics using annotate-snippet +#[derive(Setters)] pub struct AnnotateSnippetEmitter { - source_map: Option>, + #[setters(skip)] + dst: IntoDynSyncSend, + sm: Option>, + #[setters(skip)] translator: Translator, - - /// If true, hides the longer explanation text short_message: bool, - /// If true, will normalize line numbers with `LL` to prevent noise in UI test diffs. ui_testing: bool, + ignored_directories_in_source_blocks: Vec, + diagnostic_width: Option, macro_backtrace: bool, + track_diagnostics: bool, + terminal_url: TerminalUrl, + theme: OutputTheme, +} + +impl Debug for AnnotateSnippetEmitter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AnnotateSnippetEmitter") + .field("short_message", &self.short_message) + .field("ui_testing", &self.ui_testing) + .field( + "ignored_directories_in_source_blocks", + &self.ignored_directories_in_source_blocks, + ) + .field("diagnostic_width", &self.diagnostic_width) + .field("macro_backtrace", &self.macro_backtrace) + .field("track_diagnostics", &self.track_diagnostics) + .field("terminal_url", &self.terminal_url) + .field("theme", &self.theme) + .finish() + } } impl Emitter for AnnotateSnippetEmitter { @@ -38,6 +76,10 @@ impl Emitter for AnnotateSnippetEmitter { fn emit_diagnostic(&mut self, mut diag: DiagInner, _registry: &Registry) { let fluent_args = to_fluent_args(diag.args.iter()); + if self.track_diagnostics && diag.span.has_primary_spans() && !diag.span.is_dummy() { + diag.children.insert(0, diag.emitted_at_sub_diag()); + } + let mut suggestions = diag.suggestions.unwrap_tag(); self.primary_span_formatted(&mut diag.span, &mut suggestions, &fluent_args); @@ -55,12 +97,12 @@ impl Emitter for AnnotateSnippetEmitter { &diag.code, &diag.span, &diag.children, - &suggestions, + suggestions, ); } fn source_map(&self) -> Option<&SourceMap> { - self.source_map.as_deref() + self.sm.as_deref() } fn should_show_explain(&self) -> bool { @@ -70,128 +112,648 @@ impl Emitter for AnnotateSnippetEmitter { fn translator(&self) -> &Translator { &self.translator } -} -/// Provides the source string for the given `line` of `file` -fn source_string(file: Arc, line: &Line) -> String { - file.get_line(line.line_index - 1).map(|a| a.to_string()).unwrap_or_default() + fn supports_color(&self) -> bool { + false + } } -/// Maps [`crate::Level`] to [`annotate_snippets::Level`] -fn annotation_level_for_level(level: Level) -> annotate_snippets::Level { +fn annotation_level_for_level(level: Level) -> annotate_snippets::level::Level<'static> { match level { - Level::Bug | Level::Fatal | Level::Error | Level::DelayedBug => { - annotate_snippets::Level::Error + Level::Bug | Level::DelayedBug => { + annotate_snippets::Level::ERROR.with_name("error: internal compiler error") } - Level::ForceWarning | Level::Warning => annotate_snippets::Level::Warning, - Level::Note | Level::OnceNote => annotate_snippets::Level::Note, - Level::Help | Level::OnceHelp => annotate_snippets::Level::Help, - // FIXME(#59346): Not sure how to map this level - Level::FailureNote => annotate_snippets::Level::Error, + Level::Fatal | Level::Error => annotate_snippets::level::ERROR, + Level::ForceWarning | Level::Warning => annotate_snippets::Level::WARNING, + Level::Note | Level::OnceNote => annotate_snippets::Level::NOTE, + Level::Help | Level::OnceHelp => annotate_snippets::Level::HELP, + Level::FailureNote => annotate_snippets::Level::NOTE.no_name(), Level::Allow => panic!("Should not call with Allow"), Level::Expect => panic!("Should not call with Expect"), } } impl AnnotateSnippetEmitter { - pub fn new( - source_map: Option>, - translator: Translator, - short_message: bool, - macro_backtrace: bool, - ) -> Self { - Self { source_map, translator, short_message, ui_testing: false, macro_backtrace } - } - - /// Allows to modify `Self` to enable or disable the `ui_testing` flag. - /// - /// If this is set to true, line numbers will be normalized as `LL` in the output. - pub fn ui_testing(mut self, ui_testing: bool) -> Self { - self.ui_testing = ui_testing; - self + pub fn new(dst: Destination, translator: Translator) -> Self { + Self { + dst: IntoDynSyncSend(dst), + sm: None, + translator, + short_message: false, + ui_testing: false, + ignored_directories_in_source_blocks: Vec::new(), + diagnostic_width: None, + macro_backtrace: false, + track_diagnostics: false, + terminal_url: TerminalUrl::No, + theme: OutputTheme::Ascii, + } } fn emit_messages_default( &mut self, level: &Level, - messages: &[(DiagMessage, Style)], + msgs: &[(DiagMessage, Style)], args: &FluentArgs<'_>, code: &Option, msp: &MultiSpan, - _children: &[Subdiag], - _suggestions: &[CodeSuggestion], + children: &[Subdiag], + suggestions: Vec, ) { - let message = self.translator.translate_messages(messages, args); - if let Some(source_map) = &self.source_map { - // Make sure our primary file comes first - let primary_lo = if let Some(primary_span) = msp.primary_span().as_ref() { - if primary_span.is_dummy() { - // FIXME(#59346): Not sure when this is the case and what - // should be done if it happens - return; - } else { - source_map.lookup_char_pos(primary_span.lo()) + let renderer = self.renderer(); + let annotation_level = annotation_level_for_level(*level); + + // If at least one portion of the message is styled, we need to + // "pre-style" the message + let mut title = if msgs.iter().any(|(_, style)| style != &crate::Style::NoStyle) { + annotation_level + .clone() + .secondary_title(Cow::Owned(self.pre_style_msgs(msgs, *level, args))) + } else { + annotation_level.clone().primary_title(self.translator.translate_messages(msgs, args)) + }; + + if let Some(c) = code { + title = title.id(c.to_string()); + if let TerminalUrl::Yes = self.terminal_url { + title = title.id_url(format!("https://doc.rust-lang.org/error_codes/{c}.html")); + } + } + + let mut report = vec![]; + let mut group = Group::with_title(title); + + // If we don't have span information, emit and exit + let Some(sm) = self.sm.as_ref() else { + group = group.elements(children.iter().map(|c| { + let msg = self.translator.translate_messages(&c.messages, args).to_string(); + let level = annotation_level_for_level(c.level); + level.message(msg) + })); + + report.push(group); + if let Err(e) = emit_to_destination( + renderer.render(&report), + level, + &mut self.dst, + self.short_message, + ) { + panic!("failed to emit error: {e}"); + } + return; + }; + + let mut file_ann = collect_annotations(args, msp, sm, &self.translator); + + // Make sure our primary file comes first + let primary_span = msp.primary_span().unwrap_or_default(); + if !primary_span.is_dummy() { + let primary_lo = sm.lookup_char_pos(primary_span.lo()); + if let Ok(pos) = file_ann.binary_search_by(|(f, _)| f.name.cmp(&primary_lo.file.name)) { + file_ann.swap(0, pos); + } + + for (file_idx, (file, annotations)) in file_ann.into_iter().enumerate() { + if should_show_source_code(&self.ignored_directories_in_source_blocks, sm, &file) { + if let Some(snippet) = self.annotated_snippet(annotations, &file.name, sm) { + group = group.element(snippet); + } + // we can't annotate anything if the source is unavailable. + } else if !self.short_message { + // We'll just print unannotated messages + group = self.unannotated_messages( + annotations, + &file.name, + sm, + file_idx, + &mut report, + group, + &annotation_level, + ); + // If this is the last annotation for a file, and + // this is the last file, and the first child is a + // "secondary" message, we need to add padding + // ╭▸ /rustc/FAKE_PREFIX/library/core/src/clone.rs:236:13 + // │ + // ├ note: the late bound lifetime parameter + // │ (<- It adds *this*) + // ╰ warning: this was previously accepted + if let Some(c) = children.first() + && (!c.span.has_primary_spans() && !c.span.has_span_labels()) + { + group = group.element(Padding); + } } + } + } + + for c in children { + let level = annotation_level_for_level(c.level); + + // If at least one portion of the message is styled, we need to + // "pre-style" the message + let msg = if c.messages.iter().any(|(_, style)| style != &crate::Style::NoStyle) { + Cow::Owned(self.pre_style_msgs(&c.messages, c.level, args)) } else { - // FIXME(#59346): Not sure when this is the case and what - // should be done if it happens - return; + self.translator.translate_messages(&c.messages, args) }; - let mut annotated_files = FileWithAnnotatedLines::collect_annotations(self, args, msp); - if let Ok(pos) = - annotated_files.binary_search_by(|x| x.file.name.cmp(&primary_lo.file.name)) - { - annotated_files.swap(0, pos); + + // This is a secondary message with no span info + if !c.span.has_primary_spans() && !c.span.has_span_labels() { + group = group.element(level.clone().message(msg)); + continue; + } + + report.push(std::mem::replace( + &mut group, + Group::with_title(level.clone().secondary_title(msg)), + )); + + let mut file_ann = collect_annotations(args, &c.span, sm, &self.translator); + let primary_span = c.span.primary_span().unwrap_or_default(); + if !primary_span.is_dummy() { + let primary_lo = sm.lookup_char_pos(primary_span.lo()); + if let Ok(pos) = + file_ann.binary_search_by(|(f, _)| f.name.cmp(&primary_lo.file.name)) + { + file_ann.swap(0, pos); + } } - // owned: file name, line source, line index, annotations - type Owned = (String, String, usize, Vec); - let annotated_files: Vec = annotated_files - .into_iter() - .flat_map(|annotated_file| { - let file = annotated_file.file; - annotated_file - .lines + + for (file_idx, (file, annotations)) in file_ann.into_iter().enumerate() { + if should_show_source_code(&self.ignored_directories_in_source_blocks, sm, &file) { + if let Some(snippet) = self.annotated_snippet(annotations, &file.name, sm) { + group = group.element(snippet); + } + // we can't annotate anything if the source is unavailable. + } else if !self.short_message { + // We'll just print unannotated messages + group = self.unannotated_messages( + annotations, + &file.name, + sm, + file_idx, + &mut report, + group, + &level, + ); + } + } + } + + let suggestions_expected = suggestions + .iter() + .filter(|s| { + matches!( + s.style, + SuggestionStyle::HideCodeInline + | SuggestionStyle::ShowCode + | SuggestionStyle::ShowAlways + ) + }) + .count(); + for suggestion in suggestions { + match suggestion.style { + SuggestionStyle::CompletelyHidden => { + // do not display this suggestion, it is meant only for tools + } + SuggestionStyle::HideCodeAlways => { + let msg = self + .translator + .translate_messages(&[(suggestion.msg.to_owned(), Style::HeaderMsg)], args); + group = group.element(annotate_snippets::Level::HELP.message(msg)); + } + SuggestionStyle::HideCodeInline + | SuggestionStyle::ShowCode + | SuggestionStyle::ShowAlways => { + let substitutions = suggestion + .substitutions .into_iter() - .map(|line| { - // Ensure the source file is present before we try - // to load a string from it. - // FIXME(#115869): support -Z ignore-directory-in-diagnostics-source-blocks - source_map.ensure_source_file_source_present(&file); - ( - format!("{}", source_map.filename_for_diagnostics(&file.name)), - source_string(Arc::clone(&file), &line), - line.line_index, - line.annotations, - ) + .filter_map(|mut subst| { + // Suggestions coming from macros can have malformed spans. This is a heavy + // handed approach to avoid ICEs by ignoring the suggestion outright. + let invalid = + subst.parts.iter().any(|item| sm.is_valid_span(item.span).is_err()); + if invalid { + debug!("suggestion contains an invalid span: {:?}", subst); + } + + // Assumption: all spans are in the same file, and all spans + // are disjoint. Sort in ascending order. + subst.parts.sort_by_key(|part| part.span.lo()); + // Verify the assumption that all spans are disjoint + assert_eq!( + subst.parts.array_windows().find(|[a, b]| a.span.overlaps(b.span)), + None, + "all spans must be disjoint", + ); + + // 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. + subst.parts.retain(|p| is_different(sm, &p.snippet, p.span)); + + let item_span = subst.parts.first()?; + let file = sm.lookup_source_file(item_span.span.lo()); + if !invalid + && should_show_source_code( + &self.ignored_directories_in_source_blocks, + sm, + &file, + ) + { + Some(subst) + } else { + None + } + }) + .collect::>(); + + if substitutions.is_empty() { + continue; + } + let mut msg = self + .translator + .translate_message(&suggestion.msg, args) + .map_err(Report::new) + .unwrap() + .to_string(); + + let lo = substitutions + .iter() + .find_map(|sub| sub.parts.first().map(|p| p.span.lo())) + .unwrap(); + let file = sm.lookup_source_file(lo); + + let filename = + sm.filename_for_diagnostics(&file.name).to_string_lossy().to_string(); + + let other_suggestions = substitutions.len().saturating_sub(MAX_SUGGESTIONS); + + let subs = substitutions + .into_iter() + .take(MAX_SUGGESTIONS) + .filter_map(|sub| { + let mut confusion_type = ConfusionType::None; + for part in &sub.parts { + let part_confusion = + detect_confusion_type(sm, &part.snippet, part.span); + confusion_type = confusion_type.combine(part_confusion); + } + + if !matches!(confusion_type, ConfusionType::None) { + msg.push_str(confusion_type.label_text()); + } + + let mut parts = sub + .parts + .into_iter() + .filter_map(|p| { + if is_different(sm, &p.snippet, p.span) { + Some((p.span, p.snippet)) + } else { + None + } + }) + .collect::>(); + + if parts.is_empty() { + None + } else { + let spans = parts.iter().map(|(span, _)| *span).collect::>(); + // The suggestion adds an entire line of code, ending on a newline, so we'll also + // print the *following* line, to provide context of what we're advising people to + // do. Otherwise you would only see contextless code that can be confused for + // already existing code, despite the colors and UI elements. + // We special case `#[derive(_)]\n` and other attribute suggestions, because those + // are the ones where context is most useful. + let fold = if let [(p, snippet)] = &mut parts[..] + && snippet.trim().starts_with("#[") + // This allows for spaces to come between the attribute and the newline + && snippet.trim().ends_with("]") + && snippet.ends_with('\n') + && p.hi() == p.lo() + && let Ok(b) = sm.span_to_prev_source(*p) + && let b = b.rsplit_once('\n').unwrap_or_else(|| ("", &b)).1 + && b.trim().is_empty() + { + // FIXME: This is a hack: + // The span for attribute suggestions often times points to the + // beginning of an item, disregarding leading whitespace. This + // causes the attribute to be properly indented, but leaves original + // item without indentation when rendered. + // This fixes that problem by adjusting the span to point to the start + // of the whitespace, and adds the whitespace to the replacement. + // + // Source: " extern "custom" fn negate(a: i64) -> i64 {\n" + // Span: 4..4 + // Replacement: "#[unsafe(naked)]\n" + // + // Before: + // help: convert this to an `#[unsafe(naked)]` function + // | + // LL + #[unsafe(naked)] + // LL | extern "custom" fn negate(a: i64) -> i64 { + // | + // + // After + // help: convert this to an `#[unsafe(naked)]` function + // | + // LL + #[unsafe(naked)] + // LL | extern "custom" fn negate(a: i64) -> i64 { + // | + if !b.is_empty() && !snippet.ends_with(b) { + snippet.insert_str(0, b); + let offset = BytePos(b.len() as u32); + *p = p.with_lo(p.lo() - offset).shrink_to_lo(); + } + false + } else { + true + }; + + if let Some((bounding_span, source, line_offset)) = + shrink_file(spans.as_slice(), &file.name, sm) + { + let adj_lo = bounding_span.lo().to_usize(); + Some( + Snippet::source(source) + .line_start(line_offset) + .path(filename.clone()) + .fold(fold) + .patches(parts.into_iter().map( + |(span, replacement)| { + let lo = + span.lo().to_usize().saturating_sub(adj_lo); + let hi = + span.hi().to_usize().saturating_sub(adj_lo); + + Patch::new(lo..hi, replacement) + }, + )), + ) + } else { + None + } + } }) - .collect::>() - }) - .collect(); - let code = code.map(|code| code.to_string()); - - let snippets = - annotated_files.iter().map(|(file_name, source, line_index, annotations)| { - Snippet::source(source) - .line_start(*line_index) - .origin(file_name) - // FIXME(#59346): Not really sure when `fold` should be true or false - .fold(false) - .annotations(annotations.iter().map(|annotation| { - annotation_level_for_level(*level) - .span(annotation.start_col.display..annotation.end_col.display) - .label(annotation.label.as_deref().unwrap_or_default()) - })) - }); - let mut message = annotation_level_for_level(*level).title(&message).snippets(snippets); - if let Some(code) = code.as_deref() { - message = message.id(code) + .collect::>(); + if !subs.is_empty() { + report.push(std::mem::replace( + &mut group, + Group::with_title(annotate_snippets::Level::HELP.secondary_title(msg)), + )); + + group = group.elements(subs); + if other_suggestions > 0 { + group = group.element( + annotate_snippets::Level::NOTE.no_name().message(format!( + "and {} other candidate{}", + other_suggestions, + pluralize!(other_suggestions) + )), + ); + } + } + } } - // FIXME(#59346): Figure out if we can _always_ print to stderr or not. - // `emitter.rs` has the `Destination` enum that lists various possible output - // destinations. - let renderer = Renderer::plain().anonymized_line_numbers(self.ui_testing); - eprintln!("{}", renderer.render(message)) } - // FIXME(#59346): Is it ok to return None if there's no source_map? + + // FIXME: This hack should be removed once annotate_snippets is the + // default emitter. + if suggestions_expected > 0 && report.is_empty() { + group = group.element(Padding); + } + + if !group.is_empty() { + report.push(group); + } + if let Err(e) = + emit_to_destination(renderer.render(&report), level, &mut self.dst, self.short_message) + { + panic!("failed to emit error: {e}"); + } + } + + fn renderer(&self) -> Renderer { + let width = if let Some(width) = self.diagnostic_width { + width + } else if self.ui_testing || cfg!(miri) { + DEFAULT_TERM_WIDTH + } else { + termize::dimensions().map(|(w, _)| w).unwrap_or(DEFAULT_TERM_WIDTH) + }; + let decor_style = match self.theme { + OutputTheme::Ascii => annotate_snippets::renderer::DecorStyle::Ascii, + OutputTheme::Unicode => annotate_snippets::renderer::DecorStyle::Unicode, + }; + + match self.dst.current_choice() { + ColorChoice::AlwaysAnsi | ColorChoice::Always | ColorChoice::Auto => Renderer::styled(), + ColorChoice::Never => Renderer::plain(), + } + .term_width(width) + .anonymized_line_numbers(self.ui_testing) + .decor_style(decor_style) + .short_message(self.short_message) } + + fn pre_style_msgs( + &self, + msgs: &[(DiagMessage, Style)], + level: Level, + args: &FluentArgs<'_>, + ) -> String { + msgs.iter() + .filter_map(|(m, style)| { + let text = self.translator.translate_message(m, args).map_err(Report::new).unwrap(); + let style = style.anstyle(level); + if text.is_empty() { None } else { Some(format!("{style}{text}{style:#}")) } + }) + .collect() + } + + fn annotated_snippet<'a>( + &self, + annotations: Vec, + file_name: &FileName, + sm: &Arc, + ) -> Option>> { + let spans = annotations.iter().map(|a| a.span).collect::>(); + if let Some((bounding_span, source, offset_line)) = shrink_file(&spans, file_name, sm) { + let adj_lo = bounding_span.lo().to_usize(); + let filename = sm.filename_for_diagnostics(file_name).to_string_lossy().to_string(); + Some(Snippet::source(source).line_start(offset_line).path(filename).annotations( + annotations.into_iter().map(move |a| { + let lo = a.span.lo().to_usize().saturating_sub(adj_lo); + let hi = a.span.hi().to_usize().saturating_sub(adj_lo); + let ann = a.kind.span(lo..hi); + if let Some(label) = a.label { ann.label(label) } else { ann } + }), + )) + } else { + None + } + } + + fn unannotated_messages<'a>( + &self, + annotations: Vec, + file_name: &FileName, + sm: &Arc, + file_idx: usize, + report: &mut Vec>, + mut group: Group<'a>, + level: &annotate_snippets::level::Level<'static>, + ) -> Group<'a> { + let filename = sm.filename_for_diagnostics(file_name).to_string_lossy().to_string(); + let mut line_tracker = vec![]; + for (i, a) in annotations.into_iter().enumerate() { + let lo = sm.lookup_char_pos(a.span.lo()); + let hi = sm.lookup_char_pos(a.span.hi()); + if i == 0 || (a.label.is_some()) { + // Render each new file after the first in its own Group + // ╭▸ $DIR/deriving-meta-unknown-trait.rs:1:10 + // │ + // LL │ #[derive(Eqr)] + // │ ━━━ + // ╰╴ (<- It makes it so *this* will get printed) + // ╭▸ $SRC_DIR/core/src/option.rs:594:0 + // ⸬ $SRC_DIR/core/src/option.rs:602:4 + // │ + // ╰ note: not covered + if i == 0 && file_idx != 0 { + report.push(std::mem::replace(&mut group, Group::with_level(level.clone()))); + } + + if !line_tracker.contains(&lo.line) { + line_tracker.push(lo.line); + // ╭▸ $SRC_DIR/core/src/option.rs:594:0 (<- It adds *this*) + // ⸬ $SRC_DIR/core/src/option.rs:602:4 + // │ + // ╰ note: not covered + group = group.element( + Origin::path(filename.clone()) + .line(sm.doctest_offset_line(file_name, lo.line)) + .char_column(lo.col_display), + ); + } + + if hi.line > lo.line + && a.label.as_ref().is_some_and(|l| !l.is_empty()) + && !line_tracker.contains(&hi.line) + { + line_tracker.push(hi.line); + // ╭▸ $SRC_DIR/core/src/option.rs:594:0 + // ⸬ $SRC_DIR/core/src/option.rs:602:4 (<- It adds *this*) + // │ + // ╰ note: not covered + group = group.element( + Origin::path(filename.clone()) + .line(sm.doctest_offset_line(file_name, hi.line)) + .char_column(hi.col_display), + ); + } + + if let Some(label) = a.label + && !label.is_empty() + { + // ╭▸ $SRC_DIR/core/src/option.rs:594:0 + // ⸬ $SRC_DIR/core/src/option.rs:602:4 + // │ (<- It adds *this*) + // ╰ note: not covered (<- and *this*) + group = group + .element(Padding) + .element(annotate_snippets::Level::NOTE.message(label)); + } + } + } + group + } +} + +fn emit_to_destination( + rendered: String, + lvl: &Level, + dst: &mut Destination, + short_message: bool, +) -> io::Result<()> { + use crate::lock; + let _buffer_lock = lock::acquire_global_lock("rustc_errors"); + writeln!(dst, "{rendered}")?; + if !short_message && !lvl.is_failure_note() { + writeln!(dst)?; + } + dst.flush()?; + Ok(()) +} + +#[derive(Debug)] +struct Annotation { + kind: AnnotationKind, + span: Span, + label: Option, +} + +fn collect_annotations( + args: &FluentArgs<'_>, + msp: &MultiSpan, + sm: &Arc, + translator: &Translator, +) -> Vec<(Arc, Vec)> { + let mut output: Vec<(Arc, Vec)> = vec![]; + + for SpanLabel { span, is_primary, label } in msp.span_labels() { + // If we don't have a useful span, pick the primary span if that exists. + // Worst case we'll just print an error at the top of the main file. + let span = match (span.is_dummy(), msp.primary_span()) { + (_, None) | (false, _) => span, + (true, Some(span)) => span, + }; + let file = sm.lookup_source_file(span.lo()); + + let kind = if is_primary { AnnotationKind::Primary } else { AnnotationKind::Context }; + + let label = label.as_ref().map(|m| { + normalize_whitespace( + &translator.translate_message(m, args).map_err(Report::new).unwrap(), + ) + }); + + let ann = Annotation { kind, span, label }; + if sm.is_valid_span(ann.span).is_ok() { + // Look through each of our files for the one we're adding to. We + // use each files `stable_id` to avoid issues with file name + // collisions when multiple versions of the same crate are present + // in the dependency graph + if let Some((_, annotations)) = + output.iter_mut().find(|(f, _)| f.stable_id == file.stable_id) + { + annotations.push(ann); + } else { + output.push((file, vec![ann])); + } + } + } + output +} + +fn shrink_file( + spans: &[Span], + file_name: &FileName, + sm: &Arc, +) -> Option<(Span, String, usize)> { + let lo_byte = spans.iter().map(|s| s.lo()).min()?; + let lo_loc = sm.lookup_char_pos(lo_byte); + let lo = lo_loc.file.line_bounds(lo_loc.line.saturating_sub(1)).start; + + let hi_byte = spans.iter().map(|s| s.hi()).max()?; + let hi_loc = sm.lookup_char_pos(hi_byte); + let hi = lo_loc.file.line_bounds(hi_loc.line.saturating_sub(1)).end; + + let bounding_span = Span::with_root_ctxt(lo, hi); + let source = sm.span_to_snippet(bounding_span).unwrap_or_default(); + let offset_line = sm.doctest_offset_line(file_name, lo_loc.line); + + Some((bounding_span, source, offset_line)) } diff --git a/compiler/rustc_session/src/session.rs b/compiler/rustc_session/src/session.rs index f243c8be20882..522faf1db9ff1 100644 --- a/compiler/rustc_session/src/session.rs +++ b/compiler/rustc_session/src/session.rs @@ -955,7 +955,24 @@ fn default_emitter( if let HumanReadableErrorType::AnnotateSnippet = kind { let emitter = - AnnotateSnippetEmitter::new(source_map, translator, short, macro_backtrace); + AnnotateSnippetEmitter::new(stderr_destination(color_config), translator) + .sm(source_map) + .short_message(short) + .diagnostic_width(sopts.diagnostic_width) + .macro_backtrace(macro_backtrace) + .track_diagnostics(track_diagnostics) + .terminal_url(terminal_url) + .theme(if let HumanReadableErrorType::Unicode = kind { + OutputTheme::Unicode + } else { + OutputTheme::Ascii + }) + .ignored_directories_in_source_blocks( + sopts + .unstable_opts + .ignore_directory_in_diagnostics_source_blocks + .clone(), + ); Box::new(emitter.ui_testing(sopts.unstable_opts.ui_testing)) } else { let emitter = HumanEmitter::new(stderr_destination(color_config), translator) diff --git a/tests/ui/annotate-snippet/missing-type.stderr b/tests/ui/annotate-snippet/missing-type.stderr index c16f022a77fa3..5cc9cc9529f40 100644 --- a/tests/ui/annotate-snippet/missing-type.stderr +++ b/tests/ui/annotate-snippet/missing-type.stderr @@ -4,3 +4,18 @@ error[E0412]: cannot find type `Iter` in this scope LL | let x: Iter; | ^^^^ not found in this scope | +help: consider importing one of these structs + | +LL + use std::collections::binary_heap::Iter; + | +LL + use std::collections::btree_map::Iter; + | +LL + use std::collections::btree_set::Iter; + | +LL + use std::collections::hash_map::Iter; + | + = and 9 other candidates + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0412`. diff --git a/tests/ui/annotate-snippet/multiple-files.stderr b/tests/ui/annotate-snippet/multiple-files.stderr index 4236ec811d04b..ffdc9482bbf24 100644 --- a/tests/ui/annotate-snippet/multiple-files.stderr +++ b/tests/ui/annotate-snippet/multiple-files.stderr @@ -7,5 +7,8 @@ LL | other_file::WithPrivateMethod.private_method(); ::: $DIR/auxiliary/other_file.rs:5:5 | LL | fn private_method(&self) {} - | ^^^^^^^^^^^^^^^^^^^^^^^^ private method defined here - | + | ------------------------ private method defined here + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0624`. diff --git a/tests/ui/annotate-snippet/multispan.stderr b/tests/ui/annotate-snippet/multispan.stderr index baed54c59a4e9..8acb60e27a19b 100644 --- a/tests/ui/annotate-snippet/multispan.stderr +++ b/tests/ui/annotate-snippet/multispan.stderr @@ -4,39 +4,90 @@ error: hello to you, too! LL | hello!(hi); | ^^^^^^^^^^ | +note: found these 'hi's + --> $DIR/multispan.rs:15:12 + | +LL | hello!(hi); + | ^^ + = note: this error originates in the macro `hello` (in Nightly builds, run with -Z macro-backtrace for more info) + error: hello to you, too! --> $DIR/multispan.rs:18:5 | LL | hello!(hi hi); | ^^^^^^^^^^^^^ | +note: found these 'hi's + --> $DIR/multispan.rs:18:12 + | +LL | hello!(hi hi); + | ^^ ^^ + = note: this error originates in the macro `hello` (in Nightly builds, run with -Z macro-backtrace for more info) + error: hello to you, too! --> $DIR/multispan.rs:21:5 | LL | hello!(hi hi hi); | ^^^^^^^^^^^^^^^^ | +note: found these 'hi's + --> $DIR/multispan.rs:21:12 + | +LL | hello!(hi hi hi); + | ^^ ^^ ^^ + = note: this error originates in the macro `hello` (in Nightly builds, run with -Z macro-backtrace for more info) + error: hello to you, too! --> $DIR/multispan.rs:24:5 | LL | hello!(hi hey hi yo hi beep beep hi hi); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | +note: found these 'hi's + --> $DIR/multispan.rs:24:12 + | +LL | hello!(hi hey hi yo hi beep beep hi hi); + | ^^ ^^ ^^ ^^ ^^ + = note: this error originates in the macro `hello` (in Nightly builds, run with -Z macro-backtrace for more info) + error: hello to you, too! --> $DIR/multispan.rs:25:5 | LL | hello!(hi there, hi how are you? hi... hi.); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | +note: found these 'hi's + --> $DIR/multispan.rs:25:12 + | +LL | hello!(hi there, hi how are you? hi... hi.); + | ^^ ^^ ^^ ^^ + = note: this error originates in the macro `hello` (in Nightly builds, run with -Z macro-backtrace for more info) + error: hello to you, too! --> $DIR/multispan.rs:26:5 | LL | hello!(whoah. hi di hi di ho); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | +note: found these 'hi's + --> $DIR/multispan.rs:26:19 + | +LL | hello!(whoah. hi di hi di ho); + | ^^ ^^ + = note: this error originates in the macro `hello` (in Nightly builds, run with -Z macro-backtrace for more info) + error: hello to you, too! --> $DIR/multispan.rs:27:5 | LL | hello!(hi good hi and good bye); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | +note: found these 'hi's + --> $DIR/multispan.rs:27:12 + | +LL | hello!(hi good hi and good bye); + | ^^ ^^ + = note: this error originates in the macro `hello` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: aborting due to 7 previous errors + From 2db19f27dafeb3d7c6349363729fbd434f138c87 Mon Sep 17 00:00:00 2001 From: Zalathar Date: Sat, 25 Oct 2025 12:51:30 +1100 Subject: [PATCH 5/8] Don't pass `Arc` to `runtest::run` This function doesn't need its own clone of the `Arc`, and can just take a reference instead. --- src/tools/compiletest/src/executor.rs | 2 +- src/tools/compiletest/src/runtest.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/tools/compiletest/src/executor.rs b/src/tools/compiletest/src/executor.rs index c7aca6d1c5aaa..733b977872081 100644 --- a/src/tools/compiletest/src/executor.rs +++ b/src/tools/compiletest/src/executor.rs @@ -224,7 +224,7 @@ impl RunnableTest { fn run(&self, stdout: &dyn ConsoleOut, stderr: &dyn ConsoleOut) { __rust_begin_short_backtrace(|| { crate::runtest::run( - Arc::clone(&self.config), + &self.config, stdout, stderr, &self.testpaths, diff --git a/src/tools/compiletest/src/runtest.rs b/src/tools/compiletest/src/runtest.rs index ff275df9b31c0..bfae1ed370d1c 100644 --- a/src/tools/compiletest/src/runtest.rs +++ b/src/tools/compiletest/src/runtest.rs @@ -6,7 +6,6 @@ use std::hash::{DefaultHasher, Hash, Hasher}; use std::io::prelude::*; use std::io::{self, BufReader}; use std::process::{Child, Command, ExitStatus, Output, Stdio}; -use std::sync::Arc; use std::{env, fmt, iter, str}; use build_helper::fs::remove_and_create_dir_all; @@ -110,7 +109,7 @@ fn dylib_name(name: &str) -> String { } pub fn run( - config: Arc, + config: &Config, stdout: &dyn ConsoleOut, stderr: &dyn ConsoleOut, testpaths: &TestPaths, From f988679d23bae53633e58d2640ed63ca67b2ed9c Mon Sep 17 00:00:00 2001 From: Zalathar Date: Sat, 25 Oct 2025 12:49:50 +1100 Subject: [PATCH 6/8] Rename `RunnableTest` to `TestThreadArgs` and inline its methods --- src/tools/compiletest/src/executor.rs | 66 +++++++++++++-------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/src/tools/compiletest/src/executor.rs b/src/tools/compiletest/src/executor.rs index 733b977872081..06c83fbc9d074 100644 --- a/src/tools/compiletest/src/executor.rs +++ b/src/tools/compiletest/src/executor.rs @@ -107,23 +107,33 @@ fn spawn_test_thread( return None; } - let runnable_test = RunnableTest::new(test); + let args = TestThreadArgs { + config: Arc::clone(&test.config), + testpaths: test.testpaths.clone(), + revision: test.revision.clone(), + }; let should_panic = test.desc.should_panic; - let run_test = move || run_test_inner(id, should_panic, runnable_test, completion_tx); + let run_test = move || test_thread_main(id, should_panic, args, completion_tx); let thread_builder = thread::Builder::new().name(test.desc.name.clone()); let join_handle = thread_builder.spawn(run_test).unwrap(); Some(join_handle) } +struct TestThreadArgs { + config: Arc, + testpaths: TestPaths, + revision: Option, +} + /// Runs a single test, within the dedicated thread spawned by the caller. -fn run_test_inner( +fn test_thread_main( id: TestId, should_panic: ShouldPanic, - runnable_test: RunnableTest, + args: TestThreadArgs, completion_sender: mpsc::Sender, ) { - let capture = CaptureKind::for_config(&runnable_test.config); + let capture = CaptureKind::for_config(&args.config); // Install a panic-capture buffer for use by the custom panic hook. if capture.should_set_panic_hook() { @@ -133,7 +143,24 @@ fn run_test_inner( let stdout = capture.stdout(); let stderr = capture.stderr(); - let panic_payload = panic::catch_unwind(move || runnable_test.run(stdout, stderr)).err(); + // Run the test, catching any panics so that we can gracefully report + // failure (or success). + // + // FIXME(Zalathar): Ideally we would report test failures with `Result`, + // and use panics only for bugs within compiletest itself, but that would + // require a major overhaul of error handling in the test runners. + let panic_payload = panic::catch_unwind(|| { + __rust_begin_short_backtrace(|| { + crate::runtest::run( + &args.config, + stdout, + stderr, + &args.testpaths, + args.revision.as_deref(), + ); + }); + }) + .err(); if let Some(panic_buf) = panic_hook::take_capture_buf() { let panic_buf = panic_buf.lock().unwrap_or_else(|e| e.into_inner()); @@ -207,33 +234,6 @@ impl CaptureKind { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] struct TestId(usize); -struct RunnableTest { - config: Arc, - testpaths: TestPaths, - revision: Option, -} - -impl RunnableTest { - fn new(test: &CollectedTest) -> Self { - let config = Arc::clone(&test.config); - let testpaths = test.testpaths.clone(); - let revision = test.revision.clone(); - Self { config, testpaths, revision } - } - - fn run(&self, stdout: &dyn ConsoleOut, stderr: &dyn ConsoleOut) { - __rust_begin_short_backtrace(|| { - crate::runtest::run( - &self.config, - stdout, - stderr, - &self.testpaths, - self.revision.as_deref(), - ); - }); - } -} - /// Fixed frame used to clean the backtrace with `RUST_BACKTRACE=1`. #[inline(never)] fn __rust_begin_short_backtrace T>(f: F) -> T { From 665dbb373923cde80eddce31d653abd726b071a5 Mon Sep 17 00:00:00 2001 From: Zalathar Date: Sat, 25 Oct 2025 13:02:57 +1100 Subject: [PATCH 7/8] Move all other test thread args into `TestThreadArgs` --- src/tools/compiletest/src/executor.rs | 30 ++++++++++++++------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/tools/compiletest/src/executor.rs b/src/tools/compiletest/src/executor.rs index 06c83fbc9d074..d6dbdbf8639e6 100644 --- a/src/tools/compiletest/src/executor.rs +++ b/src/tools/compiletest/src/executor.rs @@ -98,41 +98,42 @@ pub(crate) fn run_tests(config: &Config, tests: Vec) -> bool { fn spawn_test_thread( id: TestId, test: &CollectedTest, - completion_tx: mpsc::Sender, + completion_sender: mpsc::Sender, ) -> Option> { if test.desc.ignore && !test.config.run_ignored { - completion_tx + completion_sender .send(TestCompletion { id, outcome: TestOutcome::Ignored, stdout: None }) .unwrap(); return None; } let args = TestThreadArgs { + id, config: Arc::clone(&test.config), testpaths: test.testpaths.clone(), revision: test.revision.clone(), + should_panic: test.desc.should_panic, + completion_sender, }; - let should_panic = test.desc.should_panic; - let run_test = move || test_thread_main(id, should_panic, args, completion_tx); - let thread_builder = thread::Builder::new().name(test.desc.name.clone()); - let join_handle = thread_builder.spawn(run_test).unwrap(); + let join_handle = thread_builder.spawn(move || test_thread_main(args)).unwrap(); Some(join_handle) } +/// All of the owned data needed by `test_thread_main`. struct TestThreadArgs { + id: TestId, + config: Arc, testpaths: TestPaths, revision: Option, + should_panic: ShouldPanic, + + completion_sender: mpsc::Sender, } /// Runs a single test, within the dedicated thread spawned by the caller. -fn test_thread_main( - id: TestId, - should_panic: ShouldPanic, - args: TestThreadArgs, - completion_sender: mpsc::Sender, -) { +fn test_thread_main(args: TestThreadArgs) { let capture = CaptureKind::for_config(&args.config); // Install a panic-capture buffer for use by the custom panic hook. @@ -168,7 +169,8 @@ fn test_thread_main( write!(stderr, "{panic_buf}"); } - let outcome = match (should_panic, panic_payload) { + // Interpret the presence/absence of a panic as test failure/success. + let outcome = match (args.should_panic, panic_payload) { (ShouldPanic::No, None) | (ShouldPanic::Yes, Some(_)) => TestOutcome::Succeeded, (ShouldPanic::No, Some(_)) => TestOutcome::Failed { message: None }, (ShouldPanic::Yes, None) => { @@ -177,7 +179,7 @@ fn test_thread_main( }; let stdout = capture.into_inner(); - completion_sender.send(TestCompletion { id, outcome, stdout }).unwrap(); + args.completion_sender.send(TestCompletion { id: args.id, outcome, stdout }).unwrap(); } enum CaptureKind { From bb20367178be12ba9afb556104df4f55dad88d40 Mon Sep 17 00:00:00 2001 From: Zalathar Date: Sat, 25 Oct 2025 13:35:58 +1100 Subject: [PATCH 8/8] Rename `ShouldPanic` to `ShouldFail` The old name was a holdover from libtest, but in compiletest we only use it for `//@ should-fail` tests, which are tests of compiletest itself. --- src/tools/compiletest/src/directives.rs | 12 +++++------ src/tools/compiletest/src/directives/tests.rs | 6 +++--- src/tools/compiletest/src/executor.rs | 21 ++++++++++--------- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/tools/compiletest/src/directives.rs b/src/tools/compiletest/src/directives.rs index 51eac58c971eb..0318ed2b3d119 100644 --- a/src/tools/compiletest/src/directives.rs +++ b/src/tools/compiletest/src/directives.rs @@ -18,7 +18,7 @@ use crate::directives::line::{DirectiveLine, line_directive}; use crate::directives::needs::CachedNeedsConditions; use crate::edition::{Edition, parse_edition}; use crate::errors::ErrorKind; -use crate::executor::{CollectedTestDesc, ShouldPanic}; +use crate::executor::{CollectedTestDesc, ShouldFail}; use crate::util::static_regex; use crate::{fatal, help}; @@ -1366,10 +1366,10 @@ pub(crate) fn make_test_description( // The `should-fail` annotation doesn't apply to pretty tests, // since we run the pretty printer across all tests by default. // If desired, we could add a `should-fail-pretty` annotation. - let should_panic = match config.mode { - TestMode::Pretty => ShouldPanic::No, - _ if should_fail => ShouldPanic::Yes, - _ => ShouldPanic::No, + let should_fail = if should_fail && config.mode != TestMode::Pretty { + ShouldFail::Yes + } else { + ShouldFail::No }; CollectedTestDesc { @@ -1377,7 +1377,7 @@ pub(crate) fn make_test_description( filterable_path: filterable_path.to_owned(), ignore, ignore_message, - should_panic, + should_fail, } } diff --git a/src/tools/compiletest/src/directives/tests.rs b/src/tools/compiletest/src/directives/tests.rs index 98249e69601be..0bf2bcd3af434 100644 --- a/src/tools/compiletest/src/directives/tests.rs +++ b/src/tools/compiletest/src/directives/tests.rs @@ -7,7 +7,7 @@ use crate::directives::{ extract_llvm_version, extract_version_range, iter_directives, line_directive, parse_edition, parse_normalize_rule, }; -use crate::executor::{CollectedTestDesc, ShouldPanic}; +use crate::executor::{CollectedTestDesc, ShouldFail}; fn make_test_description( config: &Config, @@ -247,9 +247,9 @@ fn should_fail() { let p = Utf8Path::new("a.rs"); let d = make_test_description(&config, tn.clone(), p, p, "", None); - assert_eq!(d.should_panic, ShouldPanic::No); + assert_eq!(d.should_fail, ShouldFail::No); let d = make_test_description(&config, tn, p, p, "//@ should-fail", None); - assert_eq!(d.should_panic, ShouldPanic::Yes); + assert_eq!(d.should_fail, ShouldFail::Yes); } #[test] diff --git a/src/tools/compiletest/src/executor.rs b/src/tools/compiletest/src/executor.rs index d6dbdbf8639e6..5a2136c55b05e 100644 --- a/src/tools/compiletest/src/executor.rs +++ b/src/tools/compiletest/src/executor.rs @@ -112,7 +112,7 @@ fn spawn_test_thread( config: Arc::clone(&test.config), testpaths: test.testpaths.clone(), revision: test.revision.clone(), - should_panic: test.desc.should_panic, + should_fail: test.desc.should_fail, completion_sender, }; let thread_builder = thread::Builder::new().name(test.desc.name.clone()); @@ -127,7 +127,7 @@ struct TestThreadArgs { config: Arc, testpaths: TestPaths, revision: Option, - should_panic: ShouldPanic, + should_fail: ShouldFail, completion_sender: mpsc::Sender, } @@ -170,11 +170,11 @@ fn test_thread_main(args: TestThreadArgs) { } // Interpret the presence/absence of a panic as test failure/success. - let outcome = match (args.should_panic, panic_payload) { - (ShouldPanic::No, None) | (ShouldPanic::Yes, Some(_)) => TestOutcome::Succeeded, - (ShouldPanic::No, Some(_)) => TestOutcome::Failed { message: None }, - (ShouldPanic::Yes, None) => { - TestOutcome::Failed { message: Some("test did not panic as expected") } + let outcome = match (args.should_fail, panic_payload) { + (ShouldFail::No, None) | (ShouldFail::Yes, Some(_)) => TestOutcome::Succeeded, + (ShouldFail::No, Some(_)) => TestOutcome::Failed { message: None }, + (ShouldFail::Yes, None) => { + TestOutcome::Failed { message: Some("`//@ should-fail` test did not fail as expected") } } }; @@ -338,7 +338,7 @@ pub(crate) struct CollectedTestDesc { pub(crate) filterable_path: Utf8PathBuf, pub(crate) ignore: bool, pub(crate) ignore_message: Option>, - pub(crate) should_panic: ShouldPanic, + pub(crate) should_fail: ShouldFail, } /// Whether console output should be colored or not. @@ -350,9 +350,10 @@ pub enum ColorConfig { NeverColor, } -/// Whether test is expected to panic or not. +/// Tests with `//@ should-fail` are tests of compiletest itself, and should +/// be reported as successful if and only if they would have _failed_. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -pub(crate) enum ShouldPanic { +pub(crate) enum ShouldFail { No, Yes, }