1
Fork 0

Make comma separated lists of anything easier to make for errors

Provide a new function `listify`, meant to be used in cases similar to `pluralize!`. When you have a slice of arbitrary elements that need to be presented to the user, `listify` allows you to turn that into a list of comma separated strings.

This reduces a lot of redundant logic that happens often in diagnostics.
This commit is contained in:
Esteban Küber 2025-01-31 20:36:44 +00:00
parent 7f36543a48
commit 8e9422f94e
11 changed files with 88 additions and 148 deletions

View file

@ -4,7 +4,7 @@ use std::collections::BTreeMap;
use rustc_abi::{FieldIdx, VariantIdx}; use rustc_abi::{FieldIdx, VariantIdx};
use rustc_data_structures::fx::FxIndexMap; use rustc_data_structures::fx::FxIndexMap;
use rustc_errors::{Applicability, Diag, EmissionGuarantee, MultiSpan}; use rustc_errors::{Applicability, Diag, EmissionGuarantee, MultiSpan, listify};
use rustc_hir::def::{CtorKind, Namespace}; use rustc_hir::def::{CtorKind, Namespace};
use rustc_hir::{self as hir, CoroutineKind, LangItem}; use rustc_hir::{self as hir, CoroutineKind, LangItem};
use rustc_index::IndexSlice; use rustc_index::IndexSlice;
@ -29,7 +29,7 @@ use rustc_trait_selection::error_reporting::InferCtxtErrorExt;
use rustc_trait_selection::error_reporting::traits::call_kind::{CallDesugaringKind, call_kind}; use rustc_trait_selection::error_reporting::traits::call_kind::{CallDesugaringKind, call_kind};
use rustc_trait_selection::infer::InferCtxtExt; use rustc_trait_selection::infer::InferCtxtExt;
use rustc_trait_selection::traits::{ use rustc_trait_selection::traits::{
FulfillmentErrorCode, type_known_to_meet_bound_modulo_regions, FulfillmentError, FulfillmentErrorCode, type_known_to_meet_bound_modulo_regions,
}; };
use tracing::debug; use tracing::debug;
@ -1436,17 +1436,15 @@ impl<'infcx, 'tcx> MirBorrowckCtxt<'_, 'infcx, 'tcx> {
error.obligation.predicate, error.obligation.predicate,
) )
} }
[errors @ .., last] => { _ => {
format!( format!(
"you could `clone` the value and consume it, if the \ "you could `clone` the value and consume it, if the \
following trait bounds could be satisfied: \ following trait bounds could be satisfied: {}",
{} and `{}`", listify(&errors, |e: &FulfillmentError<'tcx>| format!(
errors "`{}`",
.iter() e.obligation.predicate
.map(|e| format!("`{}`", e.obligation.predicate)) ))
.collect::<Vec<_>>() .unwrap(),
.join(", "),
last.obligation.predicate,
) )
} }
}; };

View file

@ -8,7 +8,9 @@ use rustc_ast::{
token, token,
}; };
use rustc_data_structures::fx::FxHashSet; use rustc_data_structures::fx::FxHashSet;
use rustc_errors::{Applicability, Diag, MultiSpan, PResult, SingleLabelManySpans}; use rustc_errors::{
Applicability, Diag, MultiSpan, PResult, SingleLabelManySpans, listify, pluralize,
};
use rustc_expand::base::*; use rustc_expand::base::*;
use rustc_lint_defs::builtin::NAMED_ARGUMENTS_USED_POSITIONALLY; use rustc_lint_defs::builtin::NAMED_ARGUMENTS_USED_POSITIONALLY;
use rustc_lint_defs::{BufferedEarlyLint, BuiltinLintDiag, LintId}; use rustc_lint_defs::{BufferedEarlyLint, BuiltinLintDiag, LintId};
@ -975,15 +977,11 @@ fn report_invalid_references(
} else { } else {
MultiSpan::from_spans(invalid_refs.iter().filter_map(|&(_, span, _, _)| span).collect()) MultiSpan::from_spans(invalid_refs.iter().filter_map(|&(_, span, _, _)| span).collect())
}; };
let arg_list = if let &[index] = &indexes[..] { let arg_list = format!(
format!("argument {index}") "argument{} {}",
} else { pluralize!(indexes.len()),
let tail = indexes.pop().unwrap(); listify(&indexes, |i: &usize| i.to_string()).unwrap_or_default()
format!( );
"arguments {head} and {tail}",
head = indexes.into_iter().map(|i| i.to_string()).collect::<Vec<_>>().join(", ")
)
};
e = ecx.dcx().struct_span_err( e = ecx.dcx().struct_span_err(
span, span,
format!("invalid reference to positional {arg_list} ({num_args_desc})"), format!("invalid reference to positional {arg_list} ({num_args_desc})"),

View file

@ -65,7 +65,7 @@ pub use rustc_error_messages::{
SubdiagMessage, fallback_fluent_bundle, fluent_bundle, SubdiagMessage, fallback_fluent_bundle, fluent_bundle,
}; };
use rustc_lint_defs::LintExpectationId; use rustc_lint_defs::LintExpectationId;
pub use rustc_lint_defs::{Applicability, pluralize}; pub use rustc_lint_defs::{Applicability, listify, pluralize};
use rustc_macros::{Decodable, Encodable}; use rustc_macros::{Decodable, Encodable};
pub use rustc_span::ErrorGuaranteed; pub use rustc_span::ErrorGuaranteed;
pub use rustc_span::fatal_error::{FatalError, FatalErrorMarker}; pub use rustc_span::fatal_error::{FatalError, FatalErrorMarker};
@ -1999,18 +1999,6 @@ pub fn a_or_an(s: &str) -> &'static str {
} }
} }
/// Grammatical tool for displaying messages to end users in a nice form.
///
/// Take a list ["a", "b", "c"] and output a display friendly version "a, b and c"
pub fn display_list_with_comma_and<T: std::fmt::Display>(v: &[T]) -> String {
match v {
[] => "".to_string(),
[a] => a.to_string(),
[a, b] => format!("{a} and {b}"),
[a, v @ ..] => format!("{a}, {}", display_list_with_comma_and(v)),
}
}
#[derive(Clone, Copy, PartialEq, Hash, Debug)] #[derive(Clone, Copy, PartialEq, Hash, Debug)]
pub enum TerminalUrl { pub enum TerminalUrl {
No, No,

View file

@ -3,7 +3,7 @@ use rustc_data_structures::sorted_map::SortedMap;
use rustc_data_structures::unord::UnordMap; use rustc_data_structures::unord::UnordMap;
use rustc_errors::codes::*; use rustc_errors::codes::*;
use rustc_errors::{ use rustc_errors::{
Applicability, Diag, ErrorGuaranteed, MultiSpan, pluralize, struct_span_code_err, Applicability, Diag, ErrorGuaranteed, MultiSpan, listify, pluralize, struct_span_code_err,
}; };
use rustc_hir as hir; use rustc_hir as hir;
use rustc_hir::def::{DefKind, Res}; use rustc_hir::def::{DefKind, Res};
@ -808,14 +808,10 @@ impl<'tcx> dyn HirTyLowerer<'tcx> + '_ {
.map(|(trait_, mut assocs)| { .map(|(trait_, mut assocs)| {
assocs.sort(); assocs.sort();
let trait_ = trait_.print_trait_sugared(); let trait_ = trait_.print_trait_sugared();
format!("{} in `{trait_}`", match &assocs[..] { format!(
[] => String::new(), "{} in `{trait_}`",
[only] => format!("`{only}`"), listify(&assocs[..], |a| format!("`{a}`")).unwrap_or_default()
[assocs @ .., last] => format!( )
"{} and `{last}`",
assocs.iter().map(|a| format!("`{a}`")).collect::<Vec<_>>().join(", ")
),
})
}) })
.collect::<Vec<String>>(); .collect::<Vec<String>>();
names.sort(); names.sort();
@ -1075,18 +1071,8 @@ impl<'tcx> dyn HirTyLowerer<'tcx> + '_ {
} }
}) })
.collect(); .collect();
let this_type = match &types_and_spans[..] { let this_type = listify(&types_and_spans, |(t, _)| t.to_string())
[.., _, (last, _)] => format!( .expect("expected one segment to deny");
"{} and {last}",
types_and_spans[..types_and_spans.len() - 1]
.iter()
.map(|(x, _)| x.as_str())
.intersperse(", ")
.collect::<String>()
),
[(only, _)] => only.to_string(),
[] => bug!("expected one segment to deny"),
};
let arg_spans: Vec<Span> = segments let arg_spans: Vec<Span> = segments
.clone() .clone()
@ -1102,21 +1088,9 @@ impl<'tcx> dyn HirTyLowerer<'tcx> + '_ {
ProhibitGenericsArg::Infer => kinds.push("generic"), ProhibitGenericsArg::Infer => kinds.push("generic"),
}); });
let (kind, s) = match kinds[..] { let s = pluralize!(kinds.len());
[.., _, last] => ( let kind =
format!( listify(&kinds, |k| k.to_string()).expect("expected at least one generic to prohibit");
"{} and {last}",
kinds[..kinds.len() - 1]
.iter()
.map(|&x| x)
.intersperse(", ")
.collect::<String>()
),
"s",
),
[only] => (only.to_string(), ""),
[] => bug!("expected at least one generic to prohibit"),
};
let last_span = *arg_spans.last().unwrap(); let last_span = *arg_spans.last().unwrap();
let span: MultiSpan = arg_spans.into(); let span: MultiSpan = arg_spans.into();
let mut err = struct_span_code_err!( let mut err = struct_span_code_err!(

View file

@ -1,4 +1,4 @@
use rustc_errors::{Applicability, Diag, MultiSpan}; use rustc_errors::{Applicability, Diag, MultiSpan, listify};
use rustc_hir as hir; use rustc_hir as hir;
use rustc_hir::def::Res; use rustc_hir::def::Res;
use rustc_hir::intravisit::Visitor; use rustc_hir::intravisit::Visitor;
@ -1016,18 +1016,14 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
}, },
self.tcx.def_path_str(candidate.item.container_id(self.tcx)) self.tcx.def_path_str(candidate.item.container_id(self.tcx))
), ),
[.., last] if other_methods_in_scope.len() < 5 => { _ if other_methods_in_scope.len() < 5 => {
format!( format!(
"the methods of the same name on {} and `{}`", "the methods of the same name on {}",
other_methods_in_scope[..other_methods_in_scope.len() - 1] listify(
.iter() &other_methods_in_scope[..other_methods_in_scope.len() - 1],
.map(|c| format!( |c| format!("`{}`", self.tcx.def_path_str(c.item.container_id(self.tcx)))
"`{}`", )
self.tcx.def_path_str(c.item.container_id(self.tcx)) .unwrap_or_default(),
))
.collect::<Vec<String>>()
.join(", "),
self.tcx.def_path_str(last.item.container_id(self.tcx))
) )
} }
_ => format!( _ => format!(

View file

@ -11,7 +11,7 @@ use rustc_data_structures::stack::ensure_sufficient_stack;
use rustc_data_structures::unord::UnordMap; use rustc_data_structures::unord::UnordMap;
use rustc_errors::codes::*; use rustc_errors::codes::*;
use rustc_errors::{ use rustc_errors::{
Applicability, Diag, ErrorGuaranteed, MultiSpan, StashKey, Subdiagnostic, pluralize, Applicability, Diag, ErrorGuaranteed, MultiSpan, StashKey, Subdiagnostic, listify, pluralize,
struct_span_code_err, struct_span_code_err,
}; };
use rustc_hir::def::{CtorKind, DefKind, Res}; use rustc_hir::def::{CtorKind, DefKind, Res};
@ -2190,13 +2190,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
} }
if !missing_mandatory_fields.is_empty() { if !missing_mandatory_fields.is_empty() {
let s = pluralize!(missing_mandatory_fields.len()); let s = pluralize!(missing_mandatory_fields.len());
let fields: Vec<_> = let fields = listify(&missing_mandatory_fields, |f| format!("`{f}`")).unwrap();
missing_mandatory_fields.iter().map(|f| format!("`{f}`")).collect();
let fields = match &fields[..] {
[] => unreachable!(),
[only] => only.to_string(),
[start @ .., last] => format!("{} and {last}", start.join(", ")),
};
self.dcx() self.dcx()
.struct_span_err( .struct_span_err(
span.shrink_to_hi(), span.shrink_to_hi(),
@ -2553,25 +2547,20 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
.partition(|field| field.2); .partition(|field| field.2);
err.span_labels(used_private_fields.iter().map(|(_, span, _)| *span), "private field"); err.span_labels(used_private_fields.iter().map(|(_, span, _)| *span), "private field");
if !remaining_private_fields.is_empty() { if !remaining_private_fields.is_empty() {
let remaining_private_fields_len = remaining_private_fields.len(); let names = if remaining_private_fields.len() > 6 {
let names = match &remaining_private_fields String::new()
.iter() } else {
.map(|(name, _, _)| name) format!(
.collect::<Vec<_>>()[..] "{} ",
{ listify(&remaining_private_fields, |(name, _, _)| format!("`{name}`"))
_ if remaining_private_fields_len > 6 => String::new(), .expect("expected at least one private field to report")
[name] => format!("`{name}` "), )
[names @ .., last] => {
let names = names.iter().map(|name| format!("`{name}`")).collect::<Vec<_>>();
format!("{} and `{last}` ", names.join(", "))
}
[] => bug!("expected at least one private field to report"),
}; };
err.note(format!( err.note(format!(
"{}private field{s} {names}that {were} not provided", "{}private field{s} {names}that {were} not provided",
if used_fields.is_empty() { "" } else { "...and other " }, if used_fields.is_empty() { "" } else { "...and other " },
s = pluralize!(remaining_private_fields_len), s = pluralize!(remaining_private_fields.len()),
were = pluralize!("was", remaining_private_fields_len), were = pluralize!("was", remaining_private_fields.len()),
)); ));
} }

View file

@ -4,8 +4,7 @@ use itertools::Itertools;
use rustc_data_structures::fx::FxIndexSet; use rustc_data_structures::fx::FxIndexSet;
use rustc_errors::codes::*; use rustc_errors::codes::*;
use rustc_errors::{ use rustc_errors::{
Applicability, Diag, ErrorGuaranteed, MultiSpan, StashKey, a_or_an, Applicability, Diag, ErrorGuaranteed, MultiSpan, StashKey, a_or_an, listify, pluralize,
display_list_with_comma_and, pluralize,
}; };
use rustc_hir::def::{CtorOf, DefKind, Res}; use rustc_hir::def::{CtorOf, DefKind, Res};
use rustc_hir::def_id::DefId; use rustc_hir::def_id::DefId;
@ -2462,7 +2461,8 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
param.span, param.span,
format!( format!(
"{} need{} to match the {} type of this parameter", "{} need{} to match the {} type of this parameter",
display_list_with_comma_and(&other_param_matched_names), listify(&other_param_matched_names, |n| n.to_string())
.unwrap_or_default(),
pluralize!(if other_param_matched_names.len() == 1 { pluralize!(if other_param_matched_names.len() == 1 {
0 0
} else { } else {
@ -2477,7 +2477,8 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
format!( format!(
"this parameter needs to match the {} type of {}", "this parameter needs to match the {} type of {}",
matched_ty, matched_ty,
display_list_with_comma_and(&other_param_matched_names), listify(&other_param_matched_names, |n| n.to_string())
.unwrap_or_default(),
), ),
); );
} }
@ -2523,7 +2524,8 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
generic_param.span, generic_param.span,
format!( format!(
"{} {} reference this parameter `{}`", "{} {} reference this parameter `{}`",
display_list_with_comma_and(&param_idents_matching), listify(&param_idents_matching, |n| n.to_string())
.unwrap_or_default(),
if param_idents_matching.len() == 2 { "both" } else { "all" }, if param_idents_matching.len() == 2 { "both" } else { "all" },
generic_param.name.ident().name, generic_param.name.ident().name,
), ),

View file

@ -4,7 +4,7 @@ use core::iter;
use hir::def_id::LocalDefId; use hir::def_id::LocalDefId;
use rustc_ast::util::parser::ExprPrecedence; use rustc_ast::util::parser::ExprPrecedence;
use rustc_data_structures::packed::Pu128; use rustc_data_structures::packed::Pu128;
use rustc_errors::{Applicability, Diag, MultiSpan}; use rustc_errors::{Applicability, Diag, MultiSpan, listify};
use rustc_hir::def::{CtorKind, CtorOf, DefKind, Res}; use rustc_hir::def::{CtorKind, CtorOf, DefKind, Res};
use rustc_hir::lang_items::LangItem; use rustc_hir::lang_items::LangItem;
use rustc_hir::{ use rustc_hir::{
@ -1836,16 +1836,12 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
error.obligation.predicate, error.obligation.predicate,
)); ));
} }
[errors @ .., last] => { _ => {
diag.help(format!( diag.help(format!(
"`Clone` is not implemented because the following trait bounds \ "`Clone` is not implemented because the following trait bounds \
could not be satisfied: {} and `{}`", could not be satisfied: {}",
errors listify(&errors, |e| format!("`{}`", e.obligation.predicate))
.iter() .unwrap(),
.map(|e| format!("`{}`", e.obligation.predicate))
.collect::<Vec<_>>()
.join(", "),
last.obligation.predicate,
)); ));
} }
} }

View file

@ -42,6 +42,23 @@ macro_rules! pluralize {
}; };
} }
/// Grammatical tool for displaying messages to end users in a nice form.
///
/// Take a list of items and a function to turn those items into a `String`, and output a display
/// friendly comma separated list of those items.
// FIXME(estebank): this needs to be changed to go through the translation machinery.
pub fn listify<T>(list: &[T], fmt: impl Fn(&T) -> String) -> Option<String> {
Some(match list {
[only] => fmt(&only),
[others @ .., last] => format!(
"{} and {}",
others.iter().map(|i| fmt(i)).collect::<Vec<_>>().join(", "),
fmt(&last),
),
[] => return None,
})
}
/// Indicates the confidence in the correctness of a suggestion. /// Indicates the confidence in the correctness of a suggestion.
/// ///
/// All suggestions are marked with an `Applicability`. Tools use the applicability of a suggestion /// All suggestions are marked with an `Applicability`. Tools use the applicability of a suggestion

View file

@ -5,7 +5,7 @@ use std::ops::ControlFlow;
use rustc_data_structures::fx::FxHashMap; use rustc_data_structures::fx::FxHashMap;
use rustc_errors::{ use rustc_errors::{
Applicability, Diag, DiagArgValue, IntoDiagArg, into_diag_arg_using_display, pluralize, Applicability, Diag, DiagArgValue, IntoDiagArg, into_diag_arg_using_display, listify, pluralize,
}; };
use rustc_hir::def::DefKind; use rustc_hir::def::DefKind;
use rustc_hir::def_id::DefId; use rustc_hir::def_id::DefId;
@ -362,11 +362,8 @@ pub fn suggest_constraining_type_params<'a>(
let n = trait_names.len(); let n = trait_names.len();
let stable = if all_stable { "" } else { "unstable " }; let stable = if all_stable { "" } else { "unstable " };
let trait_ = if all_known { format!("trait{}", pluralize!(n)) } else { String::new() }; let trait_ = if all_known { format!("trait{}", pluralize!(n)) } else { String::new() };
format!("{stable}{trait_}{}", match &trait_names[..] { let Some(trait_names) = listify(&trait_names, |n| n.to_string()) else { return false };
[t] => format!(" {t}"), format!("{stable}{trait_} {trait_names}")
[ts @ .., last] => format!(" {} and {last}", ts.join(", ")),
[] => return false,
},)
} else { } else {
// We're more explicit when there's a mix of stable and unstable traits. // We're more explicit when there's a mix of stable and unstable traits.
let mut trait_names = constraints let mut trait_names = constraints
@ -378,10 +375,9 @@ pub fn suggest_constraining_type_params<'a>(
.collect::<Vec<_>>(); .collect::<Vec<_>>();
trait_names.sort(); trait_names.sort();
trait_names.dedup(); trait_names.dedup();
match &trait_names[..] { match listify(&trait_names, |t| t.to_string()) {
[t] => t.to_string(), Some(names) => names,
[ts @ .., last] => format!("{} and {last}", ts.join(", ")), None => return false,
[] => return false,
} }
}; };
let constraint = constraint.join(" + "); let constraint = constraint.join(" + ");

View file

@ -24,7 +24,7 @@ use rustc_ast::MacroDef;
use rustc_ast::visit::{VisitorResult, try_visit}; use rustc_ast::visit::{VisitorResult, try_visit};
use rustc_data_structures::fx::FxHashSet; use rustc_data_structures::fx::FxHashSet;
use rustc_data_structures::intern::Interned; use rustc_data_structures::intern::Interned;
use rustc_errors::MultiSpan; use rustc_errors::{MultiSpan, listify};
use rustc_hir::def::{DefKind, Res}; use rustc_hir::def::{DefKind, Res};
use rustc_hir::def_id::{CRATE_DEF_ID, DefId, LocalDefId, LocalModDefId}; use rustc_hir::def_id::{CRATE_DEF_ID, DefId, LocalDefId, LocalModDefId};
use rustc_hir::intravisit::{self, InferKind, Visitor}; use rustc_hir::intravisit::{self, InferKind, Visitor};
@ -958,29 +958,15 @@ impl<'tcx> NamePrivacyVisitor<'tcx> {
// | ^^ field `gamma` is private # `fields.2` is `false` // | ^^ field `gamma` is private # `fields.2` is `false`
// Get the list of all private fields for the main message. // Get the list of all private fields for the main message.
let field_names: Vec<_> = fields.iter().map(|(name, _, _)| name).collect(); let Some(field_names) = listify(&fields[..], |(n, _, _)| format!("`{n}`")) else { return };
let field_names = match &field_names[..] {
[] => return,
[name] => format!("`{name}`"),
[fields @ .., last] => format!(
"{} and `{last}`",
fields.iter().map(|f| format!("`{f}`")).collect::<Vec<_>>().join(", "),
),
};
let span: MultiSpan = fields.iter().map(|(_, span, _)| *span).collect::<Vec<Span>>().into(); let span: MultiSpan = fields.iter().map(|(_, span, _)| *span).collect::<Vec<Span>>().into();
// Get the list of all private fields when pointing at the `..rest`. // Get the list of all private fields when pointing at the `..rest`.
let rest_field_names: Vec<_> = let rest_field_names: Vec<_> =
fields.iter().filter(|(_, _, is_present)| !is_present).map(|(n, _, _)| n).collect(); fields.iter().filter(|(_, _, is_present)| !is_present).map(|(n, _, _)| n).collect();
let rest_len = rest_field_names.len(); let rest_len = rest_field_names.len();
let rest_field_names = match &rest_field_names[..] { let rest_field_names =
[] => String::new(), listify(&rest_field_names[..], |n| format!("`{n}`")).unwrap_or_default();
[name] => format!("`{name}`"),
[fields @ .., last] => format!(
"{} and `{last}`",
fields.iter().map(|f| format!("`{f}`")).collect::<Vec<_>>().join(", "),
),
};
// Get all the labels for each field or `..rest` in the primary MultiSpan. // Get all the labels for each field or `..rest` in the primary MultiSpan.
let labels = fields let labels = fields
.iter() .iter()
@ -1005,7 +991,7 @@ impl<'tcx> NamePrivacyVisitor<'tcx> {
} else { } else {
None None
}, },
field_names: field_names.clone(), field_names,
variant_descr: def.variant_descr(), variant_descr: def.variant_descr(),
def_path_str: self.tcx.def_path_str(def.did()), def_path_str: self.tcx.def_path_str(def.did()),
labels, labels,