rustc_error_messages/
lib.rs

1// tidy-alphabetical-start
2#![allow(internal_features)]
3#![doc(rust_logo)]
4#![feature(rustc_attrs)]
5#![feature(rustdoc_internals)]
6#![feature(type_alias_impl_trait)]
7// tidy-alphabetical-end
8
9use std::borrow::Cow;
10use std::error::Error;
11use std::path::{Path, PathBuf};
12use std::sync::{Arc, LazyLock};
13use std::{fmt, fs, io};
14
15use fluent_bundle::FluentResource;
16pub use fluent_bundle::types::FluentType;
17pub use fluent_bundle::{self, FluentArgs, FluentError, FluentValue};
18use fluent_syntax::parser::ParserError;
19use icu_provider_adapters::fallback::{LocaleFallbackProvider, LocaleFallbacker};
20use intl_memoizer::concurrent::IntlLangMemoizer;
21use rustc_data_structures::sync::IntoDynSyncSend;
22use rustc_macros::{Decodable, Encodable};
23use rustc_span::Span;
24use tracing::{instrument, trace};
25pub use unic_langid::{LanguageIdentifier, langid};
26
27pub type FluentBundle =
28    IntoDynSyncSend<fluent_bundle::bundle::FluentBundle<FluentResource, IntlLangMemoizer>>;
29
30fn new_bundle(locales: Vec<LanguageIdentifier>) -> FluentBundle {
31    IntoDynSyncSend(fluent_bundle::bundle::FluentBundle::new_concurrent(locales))
32}
33
34#[derive(Debug)]
35pub enum TranslationBundleError {
36    /// Failed to read from `.ftl` file.
37    ReadFtl(io::Error),
38    /// Failed to parse contents of `.ftl` file.
39    ParseFtl(ParserError),
40    /// Failed to add `FluentResource` to `FluentBundle`.
41    AddResource(FluentError),
42    /// `$sysroot/share/locale/$locale` does not exist.
43    MissingLocale,
44    /// Cannot read directory entries of `$sysroot/share/locale/$locale`.
45    ReadLocalesDir(io::Error),
46    /// Cannot read directory entry of `$sysroot/share/locale/$locale`.
47    ReadLocalesDirEntry(io::Error),
48    /// `$sysroot/share/locale/$locale` is not a directory.
49    LocaleIsNotDir,
50}
51
52impl fmt::Display for TranslationBundleError {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            TranslationBundleError::ReadFtl(e) => write!(f, "could not read ftl file: {e}"),
56            TranslationBundleError::ParseFtl(e) => {
57                write!(f, "could not parse ftl file: {e}")
58            }
59            TranslationBundleError::AddResource(e) => write!(f, "failed to add resource: {e}"),
60            TranslationBundleError::MissingLocale => write!(f, "missing locale directory"),
61            TranslationBundleError::ReadLocalesDir(e) => {
62                write!(f, "could not read locales dir: {e}")
63            }
64            TranslationBundleError::ReadLocalesDirEntry(e) => {
65                write!(f, "could not read locales dir entry: {e}")
66            }
67            TranslationBundleError::LocaleIsNotDir => {
68                write!(f, "`$sysroot/share/locales/$locale` is not a directory")
69            }
70        }
71    }
72}
73
74impl Error for TranslationBundleError {
75    fn source(&self) -> Option<&(dyn Error + 'static)> {
76        match self {
77            TranslationBundleError::ReadFtl(e) => Some(e),
78            TranslationBundleError::ParseFtl(e) => Some(e),
79            TranslationBundleError::AddResource(e) => Some(e),
80            TranslationBundleError::MissingLocale => None,
81            TranslationBundleError::ReadLocalesDir(e) => Some(e),
82            TranslationBundleError::ReadLocalesDirEntry(e) => Some(e),
83            TranslationBundleError::LocaleIsNotDir => None,
84        }
85    }
86}
87
88impl From<(FluentResource, Vec<ParserError>)> for TranslationBundleError {
89    fn from((_, mut errs): (FluentResource, Vec<ParserError>)) -> Self {
90        TranslationBundleError::ParseFtl(errs.pop().expect("failed ftl parse with no errors"))
91    }
92}
93
94impl From<Vec<FluentError>> for TranslationBundleError {
95    fn from(mut errs: Vec<FluentError>) -> Self {
96        TranslationBundleError::AddResource(
97            errs.pop().expect("failed adding resource to bundle with no errors"),
98        )
99    }
100}
101
102/// Returns Fluent bundle with the user's locale resources from
103/// `$sysroot/share/locale/$requested_locale/*.ftl`.
104///
105/// If `-Z additional-ftl-path` was provided, load that resource and add it  to the bundle
106/// (overriding any conflicting messages).
107#[instrument(level = "trace")]
108pub fn fluent_bundle(
109    sysroot: PathBuf,
110    sysroot_candidates: Vec<PathBuf>,
111    requested_locale: Option<LanguageIdentifier>,
112    additional_ftl_path: Option<&Path>,
113    with_directionality_markers: bool,
114) -> Result<Option<Arc<FluentBundle>>, TranslationBundleError> {
115    if requested_locale.is_none() && additional_ftl_path.is_none() {
116        return Ok(None);
117    }
118
119    let fallback_locale = langid!("en-US");
120    let requested_fallback_locale = requested_locale.as_ref() == Some(&fallback_locale);
121    trace!(?requested_fallback_locale);
122    if requested_fallback_locale && additional_ftl_path.is_none() {
123        return Ok(None);
124    }
125    // If there is only `-Z additional-ftl-path`, assume locale is "en-US", otherwise use user
126    // provided locale.
127    let locale = requested_locale.clone().unwrap_or(fallback_locale);
128    trace!(?locale);
129    let mut bundle = new_bundle(vec![locale]);
130
131    // Add convenience functions available to ftl authors.
132    register_functions(&mut bundle);
133
134    // Fluent diagnostics can insert directionality isolation markers around interpolated variables
135    // indicating that there may be a shift from right-to-left to left-to-right text (or
136    // vice-versa). These are disabled because they are sometimes visible in the error output, but
137    // may be worth investigating in future (for example: if type names are left-to-right and the
138    // surrounding diagnostic messages are right-to-left, then these might be helpful).
139    bundle.set_use_isolating(with_directionality_markers);
140
141    // If the user requests the default locale then don't try to load anything.
142    if let Some(requested_locale) = requested_locale {
143        let mut found_resources = false;
144        for mut sysroot in Some(sysroot).into_iter().chain(sysroot_candidates.into_iter()) {
145            sysroot.push("share");
146            sysroot.push("locale");
147            sysroot.push(requested_locale.to_string());
148            trace!(?sysroot);
149
150            if !sysroot.exists() {
151                trace!("skipping");
152                continue;
153            }
154
155            if !sysroot.is_dir() {
156                return Err(TranslationBundleError::LocaleIsNotDir);
157            }
158
159            for entry in sysroot.read_dir().map_err(TranslationBundleError::ReadLocalesDir)? {
160                let entry = entry.map_err(TranslationBundleError::ReadLocalesDirEntry)?;
161                let path = entry.path();
162                trace!(?path);
163                if path.extension().and_then(|s| s.to_str()) != Some("ftl") {
164                    trace!("skipping");
165                    continue;
166                }
167
168                let resource_str =
169                    fs::read_to_string(path).map_err(TranslationBundleError::ReadFtl)?;
170                let resource =
171                    FluentResource::try_new(resource_str).map_err(TranslationBundleError::from)?;
172                trace!(?resource);
173                bundle.add_resource(resource).map_err(TranslationBundleError::from)?;
174                found_resources = true;
175            }
176        }
177
178        if !found_resources {
179            return Err(TranslationBundleError::MissingLocale);
180        }
181    }
182
183    if let Some(additional_ftl_path) = additional_ftl_path {
184        let resource_str =
185            fs::read_to_string(additional_ftl_path).map_err(TranslationBundleError::ReadFtl)?;
186        let resource =
187            FluentResource::try_new(resource_str).map_err(TranslationBundleError::from)?;
188        trace!(?resource);
189        bundle.add_resource_overriding(resource);
190    }
191
192    let bundle = Arc::new(bundle);
193    Ok(Some(bundle))
194}
195
196fn register_functions(bundle: &mut FluentBundle) {
197    bundle
198        .add_function("STREQ", |positional, _named| match positional {
199            [FluentValue::String(a), FluentValue::String(b)] => format!("{}", (a == b)).into(),
200            _ => FluentValue::Error,
201        })
202        .expect("Failed to add a function to the bundle.");
203}
204
205/// Type alias for the result of `fallback_fluent_bundle` - a reference-counted pointer to a lazily
206/// evaluated fluent bundle.
207pub type LazyFallbackBundle = Arc<LazyLock<FluentBundle, impl FnOnce() -> FluentBundle>>;
208
209/// Return the default `FluentBundle` with standard "en-US" diagnostic messages.
210#[instrument(level = "trace", skip(resources))]
211#[cfg_attr(not(bootstrap), define_opaque(LazyFallbackBundle))]
212pub fn fallback_fluent_bundle(
213    resources: Vec<&'static str>,
214    with_directionality_markers: bool,
215) -> LazyFallbackBundle {
216    Arc::new(LazyLock::new(move || {
217        let mut fallback_bundle = new_bundle(vec![langid!("en-US")]);
218
219        register_functions(&mut fallback_bundle);
220
221        // See comment in `fluent_bundle`.
222        fallback_bundle.set_use_isolating(with_directionality_markers);
223
224        for resource in resources {
225            let resource = FluentResource::try_new(resource.to_string())
226                .expect("failed to parse fallback fluent resource");
227            fallback_bundle.add_resource_overriding(resource);
228        }
229
230        fallback_bundle
231    }))
232}
233
234/// Identifier for the Fluent message/attribute corresponding to a diagnostic message.
235type FluentId = Cow<'static, str>;
236
237/// Abstraction over a message in a subdiagnostic (i.e. label, note, help, etc) to support both
238/// translatable and non-translatable diagnostic messages.
239///
240/// Translatable messages for subdiagnostics are typically attributes attached to a larger Fluent
241/// message so messages of this type must be combined with a `DiagMessage` (using
242/// `DiagMessage::with_subdiagnostic_message`) before rendering. However, subdiagnostics from
243/// the `Subdiagnostic` derive refer to Fluent identifiers directly.
244#[rustc_diagnostic_item = "SubdiagMessage"]
245pub enum SubdiagMessage {
246    /// Non-translatable diagnostic message.
247    Str(Cow<'static, str>),
248    /// Translatable message which has already been translated eagerly.
249    ///
250    /// Some diagnostics have repeated subdiagnostics where the same interpolated variables would
251    /// be instantiated multiple times with different values. These subdiagnostics' messages
252    /// are translated when they are added to the parent diagnostic, producing this variant of
253    /// `DiagMessage`.
254    Translated(Cow<'static, str>),
255    /// Identifier of a Fluent message. Instances of this variant are generated by the
256    /// `Subdiagnostic` derive.
257    FluentIdentifier(FluentId),
258    /// Attribute of a Fluent message. Needs to be combined with a Fluent identifier to produce an
259    /// actual translated message. Instances of this variant are generated by the `fluent_messages`
260    /// macro.
261    ///
262    /// <https://projectfluent.org/fluent/guide/attributes.html>
263    FluentAttr(FluentId),
264}
265
266impl From<String> for SubdiagMessage {
267    fn from(s: String) -> Self {
268        SubdiagMessage::Str(Cow::Owned(s))
269    }
270}
271impl From<&'static str> for SubdiagMessage {
272    fn from(s: &'static str) -> Self {
273        SubdiagMessage::Str(Cow::Borrowed(s))
274    }
275}
276impl From<Cow<'static, str>> for SubdiagMessage {
277    fn from(s: Cow<'static, str>) -> Self {
278        SubdiagMessage::Str(s)
279    }
280}
281
282/// Abstraction over a message in a diagnostic to support both translatable and non-translatable
283/// diagnostic messages.
284///
285/// Intended to be removed once diagnostics are entirely translatable.
286#[derive(Clone, Debug, PartialEq, Eq, Hash, Encodable, Decodable)]
287#[rustc_diagnostic_item = "DiagMessage"]
288pub enum DiagMessage {
289    /// Non-translatable diagnostic message.
290    Str(Cow<'static, str>),
291    /// Translatable message which has been already translated.
292    ///
293    /// Some diagnostics have repeated subdiagnostics where the same interpolated variables would
294    /// be instantiated multiple times with different values. These subdiagnostics' messages
295    /// are translated when they are added to the parent diagnostic, producing this variant of
296    /// `DiagMessage`.
297    Translated(Cow<'static, str>),
298    /// Identifier for a Fluent message (with optional attribute) corresponding to the diagnostic
299    /// message. Yet to be translated.
300    ///
301    /// <https://projectfluent.org/fluent/guide/hello.html>
302    /// <https://projectfluent.org/fluent/guide/attributes.html>
303    FluentIdentifier(FluentId, Option<FluentId>),
304}
305
306impl DiagMessage {
307    /// Given a `SubdiagMessage` which may contain a Fluent attribute, create a new
308    /// `DiagMessage` that combines that attribute with the Fluent identifier of `self`.
309    ///
310    /// - If the `SubdiagMessage` is non-translatable then return the message as a `DiagMessage`.
311    /// - If `self` is non-translatable then return `self`'s message.
312    pub fn with_subdiagnostic_message(&self, sub: SubdiagMessage) -> Self {
313        let attr = match sub {
314            SubdiagMessage::Str(s) => return DiagMessage::Str(s),
315            SubdiagMessage::Translated(s) => return DiagMessage::Translated(s),
316            SubdiagMessage::FluentIdentifier(id) => {
317                return DiagMessage::FluentIdentifier(id, None);
318            }
319            SubdiagMessage::FluentAttr(attr) => attr,
320        };
321
322        match self {
323            DiagMessage::Str(s) => DiagMessage::Str(s.clone()),
324            DiagMessage::Translated(s) => DiagMessage::Translated(s.clone()),
325            DiagMessage::FluentIdentifier(id, _) => {
326                DiagMessage::FluentIdentifier(id.clone(), Some(attr))
327            }
328        }
329    }
330
331    pub fn as_str(&self) -> Option<&str> {
332        match self {
333            DiagMessage::Translated(s) | DiagMessage::Str(s) => Some(s),
334            DiagMessage::FluentIdentifier(_, _) => None,
335        }
336    }
337}
338
339impl From<String> for DiagMessage {
340    fn from(s: String) -> Self {
341        DiagMessage::Str(Cow::Owned(s))
342    }
343}
344impl From<&'static str> for DiagMessage {
345    fn from(s: &'static str) -> Self {
346        DiagMessage::Str(Cow::Borrowed(s))
347    }
348}
349impl From<Cow<'static, str>> for DiagMessage {
350    fn from(s: Cow<'static, str>) -> Self {
351        DiagMessage::Str(s)
352    }
353}
354
355/// Translating *into* a subdiagnostic message from a diagnostic message is a little strange - but
356/// the subdiagnostic functions (e.g. `span_label`) take a `SubdiagMessage` and the
357/// subdiagnostic derive refers to typed identifiers that are `DiagMessage`s, so need to be
358/// able to convert between these, as much as they'll be converted back into `DiagMessage`
359/// using `with_subdiagnostic_message` eventually. Don't use this other than for the derive.
360impl From<DiagMessage> for SubdiagMessage {
361    fn from(val: DiagMessage) -> Self {
362        match val {
363            DiagMessage::Str(s) => SubdiagMessage::Str(s),
364            DiagMessage::Translated(s) => SubdiagMessage::Translated(s),
365            DiagMessage::FluentIdentifier(id, None) => SubdiagMessage::FluentIdentifier(id),
366            // There isn't really a sensible behaviour for this because it loses information but
367            // this is the most sensible of the behaviours.
368            DiagMessage::FluentIdentifier(_, Some(attr)) => SubdiagMessage::FluentAttr(attr),
369        }
370    }
371}
372
373/// A span together with some additional data.
374#[derive(Clone, Debug)]
375pub struct SpanLabel {
376    /// The span we are going to include in the final snippet.
377    pub span: Span,
378
379    /// Is this a primary span? This is the "locus" of the message,
380    /// and is indicated with a `^^^^` underline, versus `----`.
381    pub is_primary: bool,
382
383    /// What label should we attach to this span (if any)?
384    pub label: Option<DiagMessage>,
385}
386
387/// A collection of `Span`s.
388///
389/// Spans have two orthogonal attributes:
390///
391/// - They can be *primary spans*. In this case they are the locus of
392///   the error, and would be rendered with `^^^`.
393/// - They can have a *label*. In this case, the label is written next
394///   to the mark in the snippet when we render.
395#[derive(Clone, Debug, Hash, PartialEq, Eq, Encodable, Decodable)]
396pub struct MultiSpan {
397    primary_spans: Vec<Span>,
398    span_labels: Vec<(Span, DiagMessage)>,
399}
400
401impl MultiSpan {
402    #[inline]
403    pub fn new() -> MultiSpan {
404        MultiSpan { primary_spans: vec![], span_labels: vec![] }
405    }
406
407    pub fn from_span(primary_span: Span) -> MultiSpan {
408        MultiSpan { primary_spans: vec![primary_span], span_labels: vec![] }
409    }
410
411    pub fn from_spans(mut vec: Vec<Span>) -> MultiSpan {
412        vec.sort();
413        MultiSpan { primary_spans: vec, span_labels: vec![] }
414    }
415
416    pub fn push_span_label(&mut self, span: Span, label: impl Into<DiagMessage>) {
417        self.span_labels.push((span, label.into()));
418    }
419
420    /// Selects the first primary span (if any).
421    pub fn primary_span(&self) -> Option<Span> {
422        self.primary_spans.first().cloned()
423    }
424
425    /// Returns all primary spans.
426    pub fn primary_spans(&self) -> &[Span] {
427        &self.primary_spans
428    }
429
430    /// Returns `true` if any of the primary spans are displayable.
431    pub fn has_primary_spans(&self) -> bool {
432        !self.is_dummy()
433    }
434
435    /// Returns `true` if this contains only a dummy primary span with any hygienic context.
436    pub fn is_dummy(&self) -> bool {
437        self.primary_spans.iter().all(|sp| sp.is_dummy())
438    }
439
440    /// Replaces all occurrences of one Span with another. Used to move `Span`s in areas that don't
441    /// display well (like std macros). Returns whether replacements occurred.
442    pub fn replace(&mut self, before: Span, after: Span) -> bool {
443        let mut replacements_occurred = false;
444        for primary_span in &mut self.primary_spans {
445            if *primary_span == before {
446                *primary_span = after;
447                replacements_occurred = true;
448            }
449        }
450        for span_label in &mut self.span_labels {
451            if span_label.0 == before {
452                span_label.0 = after;
453                replacements_occurred = true;
454            }
455        }
456        replacements_occurred
457    }
458
459    pub fn pop_span_label(&mut self) -> Option<(Span, DiagMessage)> {
460        self.span_labels.pop()
461    }
462
463    /// Returns the strings to highlight. We always ensure that there
464    /// is an entry for each of the primary spans -- for each primary
465    /// span `P`, if there is at least one label with span `P`, we return
466    /// those labels (marked as primary). But otherwise we return
467    /// `SpanLabel` instances with empty labels.
468    pub fn span_labels(&self) -> Vec<SpanLabel> {
469        let is_primary = |span| self.primary_spans.contains(&span);
470
471        let mut span_labels = self
472            .span_labels
473            .iter()
474            .map(|&(span, ref label)| SpanLabel {
475                span,
476                is_primary: is_primary(span),
477                label: Some(label.clone()),
478            })
479            .collect::<Vec<_>>();
480
481        for &span in &self.primary_spans {
482            if !span_labels.iter().any(|sl| sl.span == span) {
483                span_labels.push(SpanLabel { span, is_primary: true, label: None });
484            }
485        }
486
487        span_labels
488    }
489
490    /// Returns `true` if any of the span labels is displayable.
491    pub fn has_span_labels(&self) -> bool {
492        self.span_labels.iter().any(|(sp, _)| !sp.is_dummy())
493    }
494
495    /// Clone this `MultiSpan` without keeping any of the span labels - sometimes a `MultiSpan` is
496    /// to be re-used in another diagnostic, but includes `span_labels` which have translated
497    /// messages. These translated messages would fail to translate without their diagnostic
498    /// arguments which are unlikely to be cloned alongside the `Span`.
499    pub fn clone_ignoring_labels(&self) -> Self {
500        Self { primary_spans: self.primary_spans.clone(), ..MultiSpan::new() }
501    }
502}
503
504impl From<Span> for MultiSpan {
505    fn from(span: Span) -> MultiSpan {
506        MultiSpan::from_span(span)
507    }
508}
509
510impl From<Vec<Span>> for MultiSpan {
511    fn from(spans: Vec<Span>) -> MultiSpan {
512        MultiSpan::from_spans(spans)
513    }
514}
515
516fn icu_locale_from_unic_langid(lang: LanguageIdentifier) -> Option<icu_locid::Locale> {
517    icu_locid::Locale::try_from_bytes(lang.to_string().as_bytes()).ok()
518}
519
520pub fn fluent_value_from_str_list_sep_by_and(l: Vec<Cow<'_, str>>) -> FluentValue<'_> {
521    // Fluent requires 'static value here for its AnyEq usages.
522    #[derive(Clone, PartialEq, Debug)]
523    struct FluentStrListSepByAnd(Vec<String>);
524
525    impl FluentType for FluentStrListSepByAnd {
526        fn duplicate(&self) -> Box<dyn FluentType + Send> {
527            Box::new(self.clone())
528        }
529
530        fn as_string(&self, intls: &intl_memoizer::IntlLangMemoizer) -> Cow<'static, str> {
531            let result = intls
532                .with_try_get::<MemoizableListFormatter, _, _>((), |list_formatter| {
533                    list_formatter.format_to_string(self.0.iter())
534                })
535                .unwrap();
536            Cow::Owned(result)
537        }
538
539        fn as_string_threadsafe(
540            &self,
541            intls: &intl_memoizer::concurrent::IntlLangMemoizer,
542        ) -> Cow<'static, str> {
543            let result = intls
544                .with_try_get::<MemoizableListFormatter, _, _>((), |list_formatter| {
545                    list_formatter.format_to_string(self.0.iter())
546                })
547                .unwrap();
548            Cow::Owned(result)
549        }
550    }
551
552    struct MemoizableListFormatter(icu_list::ListFormatter);
553
554    impl std::ops::Deref for MemoizableListFormatter {
555        type Target = icu_list::ListFormatter;
556        fn deref(&self) -> &Self::Target {
557            &self.0
558        }
559    }
560
561    impl intl_memoizer::Memoizable for MemoizableListFormatter {
562        type Args = ();
563        type Error = ();
564
565        fn construct(lang: LanguageIdentifier, _args: Self::Args) -> Result<Self, Self::Error>
566        where
567            Self: Sized,
568        {
569            let baked_data_provider = rustc_baked_icu_data::baked_data_provider();
570            let locale_fallbacker =
571                LocaleFallbacker::try_new_with_any_provider(&baked_data_provider)
572                    .expect("Failed to create fallback provider");
573            let data_provider =
574                LocaleFallbackProvider::new_with_fallbacker(baked_data_provider, locale_fallbacker);
575            let locale = icu_locale_from_unic_langid(lang)
576                .unwrap_or_else(|| rustc_baked_icu_data::supported_locales::EN);
577            let list_formatter =
578                icu_list::ListFormatter::try_new_and_with_length_with_any_provider(
579                    &data_provider,
580                    &locale.into(),
581                    icu_list::ListLength::Wide,
582                )
583                .expect("Failed to create list formatter");
584
585            Ok(MemoizableListFormatter(list_formatter))
586        }
587    }
588
589    let l = l.into_iter().map(|x| x.into_owned()).collect();
590
591    FluentValue::Custom(Box::new(FluentStrListSepByAnd(l)))
592}