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