Rollup merge of #136368 - estebank:listify, r=fee1-dead

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:
Matthias Krüger 2025-02-02 12:31:57 +01:00 committed by GitHub
commit 3559a48b8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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_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::{self as hir, CoroutineKind, LangItem};
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::infer::InferCtxtExt;
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;
@ -1434,17 +1434,15 @@ impl<'infcx, 'tcx> MirBorrowckCtxt<'_, 'infcx, 'tcx> {
error.obligation.predicate,
)
}
[errors @ .., last] => {
_ => {
format!(
"you could `clone` the value and consume it, if the \
following trait bounds could be satisfied: \
{} and `{}`",
errors
.iter()
.map(|e| format!("`{}`", e.obligation.predicate))
.collect::<Vec<_>>()
.join(", "),
last.obligation.predicate,
following trait bounds could be satisfied: {}",
listify(&errors, |e: &FulfillmentError<'tcx>| format!(
"`{}`",
e.obligation.predicate
))
.unwrap(),
)
}
};

View file

@ -8,7 +8,9 @@ use rustc_ast::{
token,
};
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_lint_defs::builtin::NAMED_ARGUMENTS_USED_POSITIONALLY;
use rustc_lint_defs::{BufferedEarlyLint, BuiltinLintDiag, LintId};
@ -975,15 +977,11 @@ fn report_invalid_references(
} else {
MultiSpan::from_spans(invalid_refs.iter().filter_map(|&(_, span, _, _)| span).collect())
};
let arg_list = if let &[index] = &indexes[..] {
format!("argument {index}")
} else {
let tail = indexes.pop().unwrap();
format!(
"arguments {head} and {tail}",
head = indexes.into_iter().map(|i| i.to_string()).collect::<Vec<_>>().join(", ")
)
};
let arg_list = format!(
"argument{} {}",
pluralize!(indexes.len()),
listify(&indexes, |i: &usize| i.to_string()).unwrap_or_default()
);
e = ecx.dcx().struct_span_err(
span,
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,
};
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};
pub use rustc_span::ErrorGuaranteed;
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)]
pub enum TerminalUrl {
No,

View file

@ -3,7 +3,7 @@ use rustc_data_structures::sorted_map::SortedMap;
use rustc_data_structures::unord::UnordMap;
use rustc_errors::codes::*;
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::def::{DefKind, Res};
@ -808,14 +808,10 @@ impl<'tcx> dyn HirTyLowerer<'tcx> + '_ {
.map(|(trait_, mut assocs)| {
assocs.sort();
let trait_ = trait_.print_trait_sugared();
format!("{} in `{trait_}`", match &assocs[..] {
[] => String::new(),
[only] => format!("`{only}`"),
[assocs @ .., last] => format!(
"{} and `{last}`",
assocs.iter().map(|a| format!("`{a}`")).collect::<Vec<_>>().join(", ")
),
})
format!(
"{} in `{trait_}`",
listify(&assocs[..], |a| format!("`{a}`")).unwrap_or_default()
)
})
.collect::<Vec<String>>();
names.sort();
@ -1075,18 +1071,8 @@ impl<'tcx> dyn HirTyLowerer<'tcx> + '_ {
}
})
.collect();
let this_type = match &types_and_spans[..] {
[.., _, (last, _)] => format!(
"{} 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 this_type = listify(&types_and_spans, |(t, _)| t.to_string())
.expect("expected one segment to deny");
let arg_spans: Vec<Span> = segments
.clone()
@ -1102,21 +1088,9 @@ impl<'tcx> dyn HirTyLowerer<'tcx> + '_ {
ProhibitGenericsArg::Infer => kinds.push("generic"),
});
let (kind, s) = match kinds[..] {
[.., _, last] => (
format!(
"{} 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 s = pluralize!(kinds.len());
let kind =
listify(&kinds, |k| k.to_string()).expect("expected at least one generic to prohibit");
let last_span = *arg_spans.last().unwrap();
let span: MultiSpan = arg_spans.into();
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::def::Res;
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))
),
[.., last] if other_methods_in_scope.len() < 5 => {
_ if other_methods_in_scope.len() < 5 => {
format!(
"the methods of the same name on {} and `{}`",
other_methods_in_scope[..other_methods_in_scope.len() - 1]
.iter()
.map(|c| format!(
"`{}`",
self.tcx.def_path_str(c.item.container_id(self.tcx))
))
.collect::<Vec<String>>()
.join(", "),
self.tcx.def_path_str(last.item.container_id(self.tcx))
"the methods of the same name on {}",
listify(
&other_methods_in_scope[..other_methods_in_scope.len() - 1],
|c| format!("`{}`", self.tcx.def_path_str(c.item.container_id(self.tcx)))
)
.unwrap_or_default(),
)
}
_ => format!(

View file

@ -11,7 +11,7 @@ use rustc_data_structures::stack::ensure_sufficient_stack;
use rustc_data_structures::unord::UnordMap;
use rustc_errors::codes::*;
use rustc_errors::{
Applicability, Diag, ErrorGuaranteed, MultiSpan, StashKey, Subdiagnostic, pluralize,
Applicability, Diag, ErrorGuaranteed, MultiSpan, StashKey, Subdiagnostic, listify, pluralize,
struct_span_code_err,
};
use rustc_hir::def::{CtorKind, DefKind, Res};
@ -2188,13 +2188,7 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
}
if !missing_mandatory_fields.is_empty() {
let s = pluralize!(missing_mandatory_fields.len());
let fields: Vec<_> =
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(", ")),
};
let fields = listify(&missing_mandatory_fields, |f| format!("`{f}`")).unwrap();
self.dcx()
.struct_span_err(
span.shrink_to_hi(),
@ -2551,25 +2545,20 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
.partition(|field| field.2);
err.span_labels(used_private_fields.iter().map(|(_, span, _)| *span), "private field");
if !remaining_private_fields.is_empty() {
let remaining_private_fields_len = remaining_private_fields.len();
let names = match &remaining_private_fields
.iter()
.map(|(name, _, _)| name)
.collect::<Vec<_>>()[..]
{
_ if remaining_private_fields_len > 6 => String::new(),
[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"),
let names = if remaining_private_fields.len() > 6 {
String::new()
} else {
format!(
"{} ",
listify(&remaining_private_fields, |(name, _, _)| format!("`{name}`"))
.expect("expected at least one private field to report")
)
};
err.note(format!(
"{}private field{s} {names}that {were} not provided",
if used_fields.is_empty() { "" } else { "...and other " },
s = pluralize!(remaining_private_fields_len),
were = pluralize!("was", remaining_private_fields_len),
s = pluralize!(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_errors::codes::*;
use rustc_errors::{
Applicability, Diag, ErrorGuaranteed, MultiSpan, StashKey, a_or_an,
display_list_with_comma_and, pluralize,
Applicability, Diag, ErrorGuaranteed, MultiSpan, StashKey, a_or_an, listify, pluralize,
};
use rustc_hir::def::{CtorOf, DefKind, Res};
use rustc_hir::def_id::DefId;
@ -2462,7 +2461,8 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
param.span,
format!(
"{} 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 {
0
} else {
@ -2477,7 +2477,8 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
format!(
"this parameter needs to match the {} type of {}",
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,
format!(
"{} {} 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" },
generic_param.name.ident().name,
),

View file

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

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.
///
/// 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_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_id::DefId;
@ -362,11 +362,8 @@ pub fn suggest_constraining_type_params<'a>(
let n = trait_names.len();
let stable = if all_stable { "" } else { "unstable " };
let trait_ = if all_known { format!("trait{}", pluralize!(n)) } else { String::new() };
format!("{stable}{trait_}{}", match &trait_names[..] {
[t] => format!(" {t}"),
[ts @ .., last] => format!(" {} and {last}", ts.join(", ")),
[] => return false,
},)
let Some(trait_names) = listify(&trait_names, |n| n.to_string()) else { return false };
format!("{stable}{trait_} {trait_names}")
} else {
// We're more explicit when there's a mix of stable and unstable traits.
let mut trait_names = constraints
@ -378,10 +375,9 @@ pub fn suggest_constraining_type_params<'a>(
.collect::<Vec<_>>();
trait_names.sort();
trait_names.dedup();
match &trait_names[..] {
[t] => t.to_string(),
[ts @ .., last] => format!("{} and {last}", ts.join(", ")),
[] => return false,
match listify(&trait_names, |t| t.to_string()) {
Some(names) => names,
None => return false,
}
};
let constraint = constraint.join(" + ");

View file

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