From c4d5a6994542218c152a22ecf4528f4e27bdde3b Mon Sep 17 00:00:00 2001 From: David Wood Date: Mon, 6 Jun 2022 14:07:55 +0100 Subject: [PATCH] diagnostics: add translation documentation - Add documentation on translation infrastructure and use of `SessionSubdiagnostic`. - Update diagnostic examples on other pages to be translatable since this is preferred. Signed-off-by: David Wood --- book.toml | 1 + src/SUMMARY.md | 7 +- src/diagnostics.md | 90 ++++--- src/diagnostics/diagnostic-items.md | 2 +- src/diagnostics/diagnostic-structs.md | 359 ++++++++++++++++++++++++++ src/diagnostics/sessiondiagnostic.md | 207 --------------- src/diagnostics/translation.md | 239 +++++++++++++++++ 7 files changed, 659 insertions(+), 246 deletions(-) create mode 100644 src/diagnostics/diagnostic-structs.md delete mode 100644 src/diagnostics/sessiondiagnostic.md create mode 100644 src/diagnostics/translation.md diff --git a/book.toml b/book.toml index 8b9f5759..c4450cf0 100644 --- a/book.toml +++ b/book.toml @@ -38,3 +38,4 @@ warning-policy = "error" [output.html.redirect] "/compiletest.html" = "tests/compiletest.html" +"/diagnostics/sessiondiagnostic.html" = "diagnostics/diagnostic-structs.html" diff --git a/src/SUMMARY.md b/src/SUMMARY.md index e74ef5e4..30297629 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -138,10 +138,11 @@ - [Two-phase-borrows](./borrow_check/two_phase_borrows.md) - [Parameter Environments](./param_env.md) - [Errors and Lints](diagnostics.md) - - [Creating Errors With SessionDiagnostic](./diagnostics/sessiondiagnostic.md) + - [Diagnostic and subdiagnostic structs](./diagnostics/diagnostic-structs.md) + - [Translation](./diagnostics/translation.md) - [`LintStore`](./diagnostics/lintstore.md) - - [Diagnostic Codes](./diagnostics/diagnostic-codes.md) - - [Diagnostic Items](./diagnostics/diagnostic-items.md) + - [Diagnostic codes](./diagnostics/diagnostic-codes.md) + - [Diagnostic items](./diagnostics/diagnostic-items.md) - [`ErrorGuaranteed`](./diagnostics/error-guaranteed.md) # MIR to Binaries diff --git a/src/diagnostics.md b/src/diagnostics.md index b6752042..7ef33d38 100644 --- a/src/diagnostics.md +++ b/src/diagnostics.md @@ -139,10 +139,11 @@ use an error-level lint instead of a fixed error. flag. That said, don't make it so terse that it's hard to understand. - The word "illegal" is illegal. Prefer "invalid" or a more specific word instead. -- Errors should document the span of code where they occur – the - [`rustc_errors::diagnostic_builder::DiagnosticBuilder`][diagbuild] `span_*` - methods allow to easily do this. Also `note` other spans that have - contributed to the error if the span isn't too large. +- Errors should document the span of code where they occur (use + [`rustc_errors::diagnostic_builder::DiagnosticBuilder`][diagbuild]'s + `span_*` methods or a diagnostic struct's `#[primary_span]` to easily do + this). Also `note` other spans that have contributed to the error if the span + isn't too large. - When emitting a message with span, try to reduce the span to the smallest amount possible that still signifies the issue - Try not to emit multiple error messages for the same error. This may require @@ -312,6 +313,15 @@ reporting errors. [errors]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_errors/index.html +Diagnostics can be implemented as types which implement the `SessionDiagnostic` +trait. This is preferred for new diagnostics as it enforces a separation +between diagnostic emitting logic and the main code paths. For less-complex +diagnostics, the `SessionDiagnostic` trait can be derived -- see [Diagnostic +structs][diagnostic-structs]. Within the trait implementation, the APIs +described below can be used as normal. + +[diagnostic-structs]: ./diagnostics/diagnostic-structs.md + [`Session`][session] and [`ParseSess`][parsesses] have methods (or fields with methods) that allow reporting errors. These methods usually have names like `span_err` or `struct_span_err` or `span_warn`, etc... @@ -327,6 +337,12 @@ directly and ones that allow finer control over what to emit. For example, [`struct_span_err`][strspanerr] instead returns a [`DiagnosticBuilder`][diagbuild]. +Most of these methods will accept strings, but it is recommended that typed +identifiers for translatable diagnostics be used for new diagnostics (see +[Translation][translation]). + +[translation]: ./diagnostics/translation.md + `DiagnosticBuilder` allows you to add related notes and suggestions to an error before emitting it by calling the [`emit`][emit] method. (Failing to either emit or [cancel][cancel] a `DiagnosticBuilder` will result in an ICE.) See the @@ -340,30 +356,30 @@ emit or [cancel][cancel] a `DiagnosticBuilder` will result in an ICE.) See the ```rust,ignore // Get a DiagnosticBuilder. This does _not_ emit an error yet. -let mut err = sess.struct_span_err(sp, "oh no! this is an error!"); +let mut err = sess.struct_span_err(sp, fluent::example::example_error); // In some cases, you might need to check if `sp` is generated by a macro to // avoid printing weird errors about macro-generated code. if let Ok(snippet) = sess.source_map().span_to_snippet(sp) { // Use the snippet to generate a suggested fix - err.span_suggestion(suggestion_sp, "try using a qux here", format!("qux {}", snippet)); + err.span_suggestion(suggestion_sp, fluent::example::try_qux_suggestion, format!("qux {}", snippet)); } else { // If we weren't able to generate a snippet, then emit a "help" message // instead of a concrete "suggestion". In practice this is unlikely to be // reached. - err.span_help(suggestion_sp, "you could use a qux here instead"); + err.span_help(suggestion_sp, fluent::example::qux_suggestion); } // emit the error err.emit(); ``` -Alternatively, for less-complex diagnostics, the `SessionDiagnostic` derive -macro can be used -- see [Creating Errors With SessionDiagnostic][sessiondiagnostic]. - -[sessiondiagnostic]: ./diagnostics/sessiondiagnostic.md - +```fluent +example-example-error = oh no! this is an error! + .try-qux-suggestion = try using a qux here + .qux-suggestion = you could use a qux here instead +``` ## Suggestions @@ -405,17 +421,17 @@ apply them) For example, to make our `qux` suggestion machine-applicable, we would do: ```rust,ignore -let mut err = sess.struct_span_err(sp, "oh no! this is an error!"); +let mut err = sess.struct_span_err(sp, fluent::example::message); if let Ok(snippet) = sess.source_map().span_to_snippet(sp) { err.span_suggestion( suggestion_sp, - "try using a qux here", + fluent::example::try_qux_suggestion, format!("qux {}", snippet), Applicability::MachineApplicable, ); } else { - err.span_help(suggestion_sp, "you could use a qux here instead"); + err.span_help(suggestion_sp, fluent::example::qux_suggestion); } err.emit(); @@ -504,9 +520,9 @@ much faster to work on. Every lint is implemented via a `struct` that implements the `LintPass` `trait` (you can also implement one of the more specific lint pass traits, either -`EarlyLintPass` or `LateLintPass` depending on when is best for your lint to run). -The trait implementation allows you to check certain syntactic constructs -as the linter walks the AST. You can then choose to emit lints in a +`EarlyLintPass` or `LateLintPass` depending on when is best for your lint to run). +The trait implementation allows you to check certain syntactic constructs +as the linter walks the AST. You can then choose to emit lints in a very similar way to compile errors. You also declare the metadata of a particular lint via the `declare_lint!` @@ -557,13 +573,12 @@ impl EarlyLintPass for WhileTrue { if let ast::ExprKind::Lit(ref lit) = pierce_parens(cond).kind { if let ast::LitKind::Bool(true) = lit.kind { if !lit.span.from_expansion() { - let msg = "denote infinite loops with `loop { ... }`"; let condition_span = cx.sess.source_map().guess_head_span(e.span); cx.struct_span_lint(WHILE_TRUE, condition_span, |lint| { - lint.build(msg) + lint.build(fluent::example::use_loop) .span_suggestion_short( condition_span, - "use `loop`", + fluent::example::suggestion, "loop".to_owned(), Applicability::MachineApplicable, ) @@ -577,6 +592,11 @@ impl EarlyLintPass for WhileTrue { } ``` +```fluent +example-use-loop = denote infinite loops with `loop {"{"} ... {"}"}` + .suggestion = use `loop` +``` + ### Edition-gated lints Sometimes we want to change the behavior of a lint in a new edition. To do this, @@ -613,15 +633,15 @@ declare_lint! { The use of the term `future-incompatible` within the compiler has a slightly broader meaning than what rustc exposes to users of the compiler. -Inside rustc, future-incompatible lints are for signalling to the user that code they have +Inside rustc, future-incompatible lints are for signalling to the user that code they have written may not compile in the future. In general, future-incompatible code exists for two reasons: -* the user has written unsound code that the compiler mistakenly accepted. While -it is within Rust's backwards compatibility guarantees to fix the soundness hole -(breaking the user's code), the lint is there to warn the user that this will happen -in some upcoming version of rustc *regardless of which edition the code uses*. This is the +* the user has written unsound code that the compiler mistakenly accepted. While +it is within Rust's backwards compatibility guarantees to fix the soundness hole +(breaking the user's code), the lint is there to warn the user that this will happen +in some upcoming version of rustc *regardless of which edition the code uses*. This is the meaning that rustc exclusively exposes to users as "future incompatible". -* the user has written code that will either no longer compiler *or* will change +* the user has written code that will either no longer compiler *or* will change meaning in an upcoming *edition*. These are often called "edition lints" and can be typically seen in the various "edition compatibility" lint groups (e.g., `rust_2021_compatibility`) that are used to lint against code that will break if the user updates the crate's edition. @@ -644,11 +664,11 @@ declare_lint! { Notice the `reason` field which describes why the future incompatible change is happening. This will change the diagnostic message the user receives as well as determine which lint groups the lint is added to. In the example above, the lint is an "edition lint" -(since it's "reason" is `EditionError`) signifying to the user that the use of anonymous +(since it's "reason" is `EditionError`) signifying to the user that the use of anonymous parameters will no longer compile in Rust 2018 and beyond. -Inside [LintStore::register_lints][fi-lint-groupings], lints with `future_incompatible` -fields get placed into either edition-based lint groups (if their `reason` is tied to +Inside [LintStore::register_lints][fi-lint-groupings], lints with `future_incompatible` +fields get placed into either edition-based lint groups (if their `reason` is tied to an edition) or into the `future_incompatibility` lint group. [fi-lint-groupings]: https://github.com/rust-lang/rust/blob/51fd129ac12d5bfeca7d216c47b0e337bf13e0c2/compiler/rustc_lint/src/context.rs#L212-L237 @@ -659,7 +679,7 @@ to support this. ### Renaming or removing a lint -If it is determined that a lint is either improperly named or no longer needed, +If it is determined that a lint is either improperly named or no longer needed, the lint must be registered for renaming or removal, which will trigger a warning if a user tries to use the old lint name. To declare a rename/remove, add a line with [`store.register_renamed`] or [`store.register_removed`] to the code of the @@ -695,11 +715,11 @@ This defines the `nonstandard_style` group which turns on the listed lints. A user can turn on these lints with a `!#[warn(nonstandard_style)]` attribute in the source code, or by passing `-W nonstandard-style` on the command line. -Some lint groups are created automatically in `LintStore::register_lints`. For instance, -any lint declared with `FutureIncompatibleInfo` where the reason is -`FutureIncompatibilityReason::FutureReleaseError` (the default when +Some lint groups are created automatically in `LintStore::register_lints`. For instance, +any lint declared with `FutureIncompatibleInfo` where the reason is +`FutureIncompatibilityReason::FutureReleaseError` (the default when `@future_incompatible` is used in `declare_lint!`), will be added to -the `future_incompatible` lint group. Editions also have their own lint groups +the `future_incompatible` lint group. Editions also have their own lint groups (e.g., `rust_2021_compatibility`) automatically generated for any lints signaling future-incompatible code that will break in the specified edition. diff --git a/src/diagnostics/diagnostic-items.md b/src/diagnostics/diagnostic-items.md index af11eb9e..0031ba28 100644 --- a/src/diagnostics/diagnostic-items.md +++ b/src/diagnostics/diagnostic-items.md @@ -76,7 +76,7 @@ In rustc, diagnostic items are looked up via [`Symbol`]s from inside the [`rustc_span::symbol::sym`] module. These can then be mapped to [`DefId`]s using [`TyCtxt::get_diagnostic_item()`] or checked if they match a [`DefId`] using [`TyCtxt::is_diagnostic_item()`]. When mapping from a diagnostic item to -a [`DefId`] the method will return a `Option`. This can be `None` if +a [`DefId`], the method will return a `Option`. This can be `None` if either the symbol isn't a diagnostic item or the type is not registered, for instance when compiling with `#[no_std]`. All following examples are based on [`DefId`]s and their usage. diff --git a/src/diagnostics/diagnostic-structs.md b/src/diagnostics/diagnostic-structs.md new file mode 100644 index 00000000..cab9b0c2 --- /dev/null +++ b/src/diagnostics/diagnostic-structs.md @@ -0,0 +1,359 @@ +# Diagnostic and subdiagnostic structs +rustc has two diagnostic derives that can be used to create simple diagnostics, +which are recommended to be used when they are applicable: +`#[derive(SessionDiagnostic)]` and `#[derive(SessionSubdiagnostic)]`. + +Diagnostics created with the derive macros can be translated into different +languages and each has a slug that uniquely identifies the diagnostic. + +## `#[derive(SessionDiagnostic)]` +Instead of using the `DiagnosticBuilder` API to create and emit diagnostics, +the `SessionDiagnostic` derive can be used. `#[derive(SessionDiagnostic)]` is +only applicable for simple diagnostics that don't require much logic in +deciding whether or not to add additional subdiagnostics. + +Consider the [definition][defn] of the "field already declared" diagnostic +shown below: + +```rust,ignore +#[derive(SessionDiagnostic)] +#[error(code = "E0124", slug = "typeck-field-already-declared")] +pub struct FieldAlreadyDeclared { + pub field_name: Ident, + #[primary_span] + #[label] + pub span: Span, + #[label = "previous-decl-label"] + pub prev_span: Span, +} +``` + +`SessionDiagnostic` can only be applied to structs. Every `SessionDiagnostic` +has to have one attribute applied to the struct itself: either `#[error(..)]` +for defining errors, or `#[warning(..)]` for defining warnings. + +If an error has an error code (e.g. "E0624"), then that can be specified using +the `code` sub-attribute. Specifying a `code` isn't mandatory, but if you are +porting a diagnostic that uses `DiagnosticBuilder` to use `SessionDiagnostic` +then you should keep the code if there was one. + +Both `#[error(..)]` and `#[warning(..)]` must set a value for the `slug` +sub-attribute. `slug` uniquely identifies the diagnostic and is also how the +compiler knows what error message to emit (in the default locale of the +compiler, or in the locale requested by the user). See [translation +documentation](./translation.md) to learn more about how translatable error +messages are written. + +In our example, the Fluent message for the "field already declared" diagnostic +looks like this: + +```fluent +typeck-field-already-declared = + field `{$field_name}` is already declared + .label = field already declared + .previous-decl-label = `{$field_name}` first declared here +``` + +`typeck-field-already-declared` is the `slug` from our example and is followed +by the diagnostic message. + +Every field of the `SessionDiagnostic` which does not have an annotation is +available in Fluent messages as a variable, like `field_name` in the example +above. Fields can be annotated `#[skip_arg]` if this is undesired. + +Using the `#[primary_span]` attribute on a field (that has type `Span`) +indicates the primary span of the diagnostic which will have the main message +of the diagnostic. + +Diagnostics are more than just their primary message, they often include +labels, notes, help messages and suggestions, all of which can also be +specified on a `SessionDiagnostic`. + +`#[label]`, `#[help]` and `#[note]` can all be applied to fields which have the +type `Span`. Applying any of these attributes will create the corresponding +subdiagnostic with that `Span`. These attributes will look for their +diagnostic message in a Fluent attribute attached to the primary Fluent +message. In our example, `#[label]` will look for +`typeck-field-already-declared.label` (which has the message "field already +declared"). If there is more than one subdiagnostic of the same type, then +these attributes can also take a value that is the attribute name to look for +(e.g. `previous-decl-label` in our example). + +Other types have special behavior when used in a `SessionDiagnostic` derive: + +- Any attribute applied to an `Option` and will only emit a + subdiagnostic if the option is `Some(..)`. +- Any attribute applied to a `Vec` will be repeated for each element of the + vector. + +`#[help]` and `#[note]` can also be applied to the struct itself, in which case +they work exactly like when applied to fields except the subdiagnostic won't +have a `Span`. These attributes can also be applied to fields of type `()` for +the same effect, which when combined with the `Option` type can be used to +represent optional `#[note]`/`#[help]` subdiagnostics. + +Suggestions can be emitted using one of four field attributes: + +- `#[suggestion(message = "...", code = "...", applicability = "...")]` +- `#[suggestion_hidden(message = "...", code = "...", applicability = "...")]` +- `#[suggestion_short(message = "...", code = "...", applicability = "...")]` +- `#[suggestion_verbose(message = "...", code = "...", applicability = "...")]` + +Suggestions must be applied on either a `Span` field or a `(Span, +MachineApplicability)` field. Similarly to other field attributes, `message` +specifies the Fluent attribute with the message and defaults to `.suggestion`. +`code` specifies the code that should be suggested as a replacement and is a +format string (e.g. `{field_name}` would be replaced by the value of the +`field_name` field of the struct), not a Fluent identifier. `applicability` can +be used to specify the applicability in the attribute, it cannot be used when +the field's type contains an `Applicability`. + +In the end, the `SessionDiagnostic` derive will generate an implementation of +`SessionDiagnostic` that looks like the following: + +```rust,ignore +impl SessionDiagnostic for FieldAlreadyDeclared { + fn into_diagnostic(self, sess: &'_ rustc_session::Session) -> DiagnosticBuilder<'_> { + let mut diag = sess.struct_err_with_code( + rustc_errors::DiagnosticMessage::fluent("typeck-field-already-declared"), + rustc_errors::DiagnosticId::Error("E0124") + ); + diag.set_span(self.span); + diag.span_label( + self.span, + rustc_errors::DiagnosticMessage::fluent_attr("typeck-field-already-declared", "label") + ); + diag.span_label( + self.prev_span, + rustc_errors::DiagnosticMessage::fluent_attr("typeck-field-already-declared", "previous-decl-label") + ); + diag + } +} +``` + +Now that we've defined our diagnostic, how do we [use it][use]? It's quite +straightforward, just create an instance of the struct and pass it to +`emit_err` (or `emit_warning`): + +```rust,ignore +tcx.sess.emit_err(FieldAlreadyDeclared { + field_name: f.ident, + span: f.span, + prev_span, +}); +``` + +### Reference +`#[derive(SessionDiagnostic)]` supports the following attributes: + +- `#[error(code = "...", slug = "...")]` or `#[warning(code = "...", slug = "...")]` + - _Applied to struct._ + - _Mandatory_ + - Defines the struct to be representing an error or a warning. + - `code = "..."` (_Optional_) + - Specifies the error code. + - `slug = "..."` (_Mandatory_) + - Uniquely identifies the diagnostic and corresponds to its Fluent message, + mandatory. +- `#[note]` or `#[note = "..."]` (_Optional_) + - _Applied to struct or `Span`/`()` fields._ + - Adds a note subdiagnostic. + - Value is the Fluent attribute (relative to the Fluent message specified by + `slug`) for the note's message + - Defaults to `note`. + - If applied to a `Span` field, creates a spanned note. +- `#[help]` or `#[help = "..."]` (_Optional_) + - _Applied to struct or `Span`/`()` fields._ + - Adds a help subdiagnostic. + - Value is the Fluent attribute (relative to the Fluent message specified by + `slug`) for the help's message. + - Defaults to `help`. + - If applied to a `Span` field, creates a spanned help. +- `#[label]` or `#[label = "..."]` (_Optional_) + - _Applied to `Span` fields._ + - Adds a label subdiagnostic. + - Value is the Fluent attribute (relative to the Fluent message specified by + `slug`) for the label's message. + - Defaults to `label`. +- `#[suggestion{,_hidden,_short,_verbose}(message = "...", code = "...", applicability = "...")]` + (_Optional_) + - _Applied to `(Span, MachineApplicability)` or `Span` fields._ + - Adds a suggestion subdiagnostic. + - `message = "..."` (_Mandatory_) + - Value is the Fluent attribute (relative to the Fluent message specified + by `slug`) for the suggestion's message. + - Defaults to `suggestion`. + - `code = "..."` (_Mandatory_) + - Value is a format string indicating the code to be suggested as a + replacement. + - `applicability = "..."` (_Optional_) + - String which must be one of `machine-applicable`, `maybe-incorrect`, + `has-placeholders` or `unspecified`. +- `#[subdiagnostic]` + - _Applied to a type that implements `AddToDiagnostic` (from + `#[derive(SessionSubdiagnostic)]`)._ + - Adds the subdiagnostic represented by the subdiagnostic struct. +- `#[primary_span]` (_Optional_) + - _Applied to `Span` fields._ + - Indicates the primary span of the diagnostic. +- `#[skip_arg]` (_Optional_) + - _Applied to any field._ + - Prevents the field from being provided as a diagnostic argument. + +## `#[derive(SessionSubdiagnostic)]` +It is common in the compiler to write a function that conditionally adds a +specific subdiagnostic to an error if it is applicable. Oftentimes these +subdiagnostics could be represented using a diagnostic struct even if the +overall diagnostic could not. In this circumstance, the `SessionSubdiagnostic` +derive can be used to represent a partial diagnostic (e.g a note, label, help or +suggestion) as a struct. + +Consider the [definition][subdiag_defn] of the "expected return type" label +shown below: + +```rust +#[derive(SessionSubdiagnostic)] +pub enum ExpectedReturnTypeLabel<'tcx> { + #[label(slug = "typeck-expected-default-return-type")] + Unit { + #[primary_span] + span: Span, + }, + #[label(slug = "typeck-expected-return-type")] + Other { + #[primary_span] + span: Span, + expected: Ty<'tcx>, + }, +} +``` + +Unlike `SessionDiagnostic`, `SessionSubdiagnostic` can be applied to structs or +enums. Attributes that are placed on the type for structs are placed on each +variants for enums (or vice versa). Each `SessionSubdiagnostic` should have one +attribute applied to the struct or each variant, one of: + +- `#[label(..)]` for defining a label +- `#[note(..)]` for defining a note +- `#[help(..)]` for defining a help +- `#[suggestion{,_hidden,_short,_verbose}(..)]` for defining a suggestion + +All of the above must have a value set for the `slug` sub-attribute. `slug` +uniquely identifies the diagnostic and is also how the compiler knows what +error message to emit (in the default locale of the compiler, or in the locale +requested by the user). See [translation documentation](./translation.md) to +learn more about how translatable error messages are written. + +In our example, the Fluent message for the "expected return type" label +looks like this: + +```fluent +typeck-expected-default-return-type = expected `()` because of default return type + +typeck-expected-return-type = expected `{$expected}` because of return type +``` + +Using the `#[primary_span]` attribute on a field (with type `Span`) will denote +the primary span of the subdiagnostic. A primary span is only necessary for a +label or suggestion, which can not be spanless. + +Every field of the type/variant which does not have an annotation is available +in Fluent messages as a variable. Fields can be annotated `#[skip_arg]` if this +is undesired. + +Like `SessionDiagnostic`, `SessionSubdiagnostic` supports `Option` and +`Vec` fields. + +Suggestions can be emitted using one of four attributes on the type/variant: + +- `#[suggestion(message = "...", code = "...", applicability = "...")]` +- `#[suggestion_hidden(message = "...", code = "...", applicability = "...")]` +- `#[suggestion_short(message = "...", code = "...", applicability = "...")]` +- `#[suggestion_verbose(message = "...", code = "...", applicability = "...")]` + +Suggestions require `#[primary_span]` be set on a field and can have the +following sub-attributes: + +- `message` specifies the Fluent attribute with the message and defaults to + `.suggestion`. +- `code` specifies the code that should be suggested as a replacement and is a + format string (e.g. `{field_name}` would be replaced by the value of the + `field_name` field of the struct), not a Fluent identifier. +- `applicability` can be used to specify the applicability in the attribute, it + cannot be used when the field's type contains an `Applicability`. + +Applicabilities can also be specified as a field (of type `Applicability`) +using the `#[applicability]` attribute. + +In the end, the `SessionSubdiagnostic` derive will generate an implementation +of `SessionSubdiagnostic` that looks like the following: + +```rust +impl<'tcx> AddToDiagnostic for ExpectedReturnTypeLabel<'tcx> { + fn add_to_diagnostic(self, diag: &mut rustc_errors::Diagnostic) { + use rustc_errors::{Applicability, IntoDiagnosticArg}; + match self { + ExpectedReturnTypeLabel::Unit { span } => { + diag.span_label(span, DiagnosticMessage::fluent("typeck-expected-default-return-type")) + } + ExpectedReturnTypeLabel::Other { span, expected } => { + diag.set_arg("expected", expected); + diag.span_label(span, DiagnosticMessage::fluent("typeck-expected-return-type")) + } + + } + } +} +``` + +Once defined, a subdiagnostic can be used by passing it to the `subdiagnostic` +function ([example][subdiag_use_1] and [example][subdiag_use_2]) on a +diagnostic or by assigning it to a `#[subdiagnostic]`-annotated field of a +diagnostic struct. + +### Reference +`#[derive(SessionSubdiagnostic)]` supports the following attributes: + +- `#[label(slug = "...")]`, `#[help(slug = "...")]` or `#[note(slug = "...")]` + - _Applied to struct or enum variant. Mutually exclusive with struct/enum variant attributes._ + - _Mandatory_ + - Defines the type to be representing a label, help or note. + - `slug = "..."` (_Mandatory_) + - Uniquely identifies the diagnostic and corresponds to its Fluent message, + mandatory. +- `#[suggestion{,_hidden,_short,_verbose}(message = "...", code = "...", applicability = "...")]` + - _Applied to struct or enum variant. Mutually exclusive with struct/enum variant attributes._ + - _Mandatory_ + - Defines the type to be representing a suggestion. + - `message = "..."` (_Mandatory_) + - Value is the Fluent attribute (relative to the Fluent message specified + by `slug`) for the suggestion's message. + - Defaults to `suggestion`. + - `code = "..."` (_Mandatory_) + - Value is a format string indicating the code to be suggested as a + replacement. + - `applicability = "..."` (_Optional_) + - _Mutually exclusive with `#[applicability]` on a field._ + - Value is the applicability of the suggestion. + - String which must be one of: + - `machine-applicable` + - `maybe-incorrect` + - `has-placeholders` + - `unspecified` +- `#[primary_span]` (_Mandatory_ for labels and suggestions; _optional_ otherwise) + - _Applied to `Span` fields._ + - Indicates the primary span of the subdiagnostic. +- `#[applicability]` (_Optional_; only applicable to suggestions) + - _Applied to `Applicability` fields._ + - Indicates the applicability of the suggestion. +- `#[skip_arg]` (_Optional_) + - _Applied to any field._ + - Prevents the field from being provided as a diagnostic argument. + +[defn]: https://github.com/rust-lang/rust/blob/bbe9d27b8ff36da56638aa43d6d0cdfdf89a4e57/compiler/rustc_typeck/src/errors.rs#L65-L74 +[use]: https://github.com/rust-lang/rust/blob/eb82facb1626166188d49599a3313fc95201f556/compiler/rustc_typeck/src/collect.rs#L981-L985 + +[subdiag_defn]: https://github.com/rust-lang/rust/blob/e70c60d34b9783a2fd3171d88d248c2e0ec8ecdd/compiler/rustc_typeck/src/errors.rs#L220-L233 +[subdiag_use_1]: https://github.com/rust-lang/rust/blob/e70c60d34b9783a2fd3171d88d248c2e0ec8ecdd/compiler/rustc_typeck/src/check/fn_ctxt/suggestions.rs#L556-L560 +[subdiag_use_2]: https://github.com/rust-lang/rust/blob/e70c60d34b9783a2fd3171d88d248c2e0ec8ecdd/compiler/rustc_typeck/src/check/fn_ctxt/suggestions.rs#L575-L579 diff --git a/src/diagnostics/sessiondiagnostic.md b/src/diagnostics/sessiondiagnostic.md deleted file mode 100644 index e82d5629..00000000 --- a/src/diagnostics/sessiondiagnostic.md +++ /dev/null @@ -1,207 +0,0 @@ -# Creating translatable errors using `SessionDiagnostic` -The `SessionDiagnostic` derive macro is the recommended way to create -diagnostics. Diagnostics created with the derive macro can be translated into -different languages and each have a slug that uniquely identifies the -diagnostic. - -Instead of using the `DiagnosticBuilder` API to create and emit diagnostics, -the `SessionDiagnostic` derive macro is applied to structs. - -The [definition][defn] of the "field already declared" diagnostic is shown -below. - -```rust,ignore -#[derive(SessionDiagnostic)] -#[error(code = "E0124", slug = "typeck-field-already-declared")] -pub struct FieldAlreadyDeclared { - pub field_name: Ident, - #[primary_span] - #[label] - pub span: Span, - #[label = "previous-decl-label"] - pub prev_span: Span, -} -``` - -Every `SessionDiagnostic` has to have one attribute applied to the struct -itself: either `#[error(..)]` for defining errors, or `#[warning(..)]` for -defining warnings. - -If an error has an error code (e.g. "E0624"), then that can be specified using -the `code` sub-attribute. Specifying a `code` isn't mandatory, but if you are -porting a diagnostic that uses `DiagnosticBuilder` to use `SessionDiagnostic` -then you should keep the code if there was one. - -Both `#[error(..)]` and `#[warning(..)]` must set a value for the `slug` -sub-attribute. `slug` uniquely identifies the diagnostic and is also how the -compiler knows what error message to emit (in the default locale of the -compiler, or in the locale requested by the user). - -rustc uses [Fluent](https://projectfluent.org) to handle the intricacies of -translation. Each diagnostic's `slug` is actually an identifier for a *Fluent -message*. Let's take a look at what the Fluent message for the "field already -declared" diagnostic looks like: - -```fluent -typeck-field-already-declared = - field `{$field_name}` is already declared - .label = field already declared - .previous-decl-label = `{$field_name}` first declared here -``` - -`typeck-field-already-declared` is the `slug` from our example and is followed -by the diagnostic message. - -Fluent is built around the idea of "asymmetric localization", which aims to -decouple the expressiveness of translations from the grammar of the source -language (English in rustc's case). Prior to translation, rustc's diagnostics -relied heavily on interpolation to build the messages shown to the users. -Interpolated strings are hard to translate because writing a natural-sounding -translation might require more, less, or just different interpolation than the -English string, all of which would require changes to the compiler's source -code to support. - -As the compiler team gain more experience creating `SessionDiagnostic` structs -that have all of the information necessary to be translated into different -languages, this page will be updated with more guidance. For now, the [Project -Fluent](https://projectfluent.org) documentation has excellent examples of -translating messages into different locales and the information that needs to -be provided by the code to do so. - -When adding or changing a diagnostic, you don't need to worry about the -translations, only updating the original English message. All of rustc's -English Fluent messages can be found in -`/compiler/rustc_error_messages/locales/en-US/diagnostics.ftl`. - -Every field of the `SessionDiagnostic` which does not have an annotation is -available in Fluent messages as a variable, like `field_name` in the example -above. - -Using the `#[primary_span]` attribute on a field (that has type `Span`) -indicates the primary span of the diagnostic which will have the main message -of the diagnostic. - -Diagnostics are more than just their primary message, they often include -labels, notes, help messages and suggestions, all of which can also be -specified on a `SessionDiagnostic`. - -`#[label]`, `#[help]` and `#[note]` can all be applied to fields which have the -type `Span`. Applying any of these attributes will create the corresponding -sub-diagnostic with that `Span`. These attributes will look for their -diagnostic message in a Fluent attribute attached to the primary Fluent -message. In our example, `#[label]` will look for -`typeck-field-already-declared.label` (which has the message "field already -declared"). If there is more than one sub-diagnostic of the same type, then -these attributes can also take a value that is the attribute name to look for -(e.g. `previous-decl-label` in our example). - -`#[help]` and `#[note]` can also be applied to the struct itself, in which case -they work exactly like when applied to fields except the sub-diagnostic won't -have a `Span`. - -Any attribute can also be applied to an `Option` and will only emit a -sub-diagnostic if the option is `Some(..)`. - -Suggestions can be emitted using one of four field attributes: - -- `#[suggestion(message = "...", code = "...")]` -- `#[suggestion_hidden(message = "...", code = "...")]` -- `#[suggestion_short(message = "...", code = "...")]` -- `#[suggestion_verbose(message = "...", code = "...")]` - -Suggestions must be applied on either a `Span` field or a -`(Span, MachineApplicability)` field. Similarly to other field attributes, -`message` specifies the Fluent attribute with the message and defaults to -`.suggestion`. `code` specifies the code that should be suggested as a -replacement and is a format string (e.g. `{field_name}` would be replaced by -the value of the `field_name` field of the struct), not a Fluent identifier. - -In the end, the `SessionDiagnostic` derive will generate an implementation of -`SessionDiagnostic` that looks like the following: - -```rust,ignore -impl SessionDiagnostic for FieldAlreadyDeclared { - fn into_diagnostic(self, sess: &'_ rustc_session::Session) -> DiagnosticBuilder<'_> { - let mut diag = sess.struct_err_with_code( - rustc_errors::DiagnosticMessage::fluent("typeck-field-already-declared"), - rustc_errors::DiagnosticId::Error("E0124") - ); - diag.set_span(self.span); - diag.span_label( - self.span, - rustc_errors::DiagnosticMessage::fluent_attr("typeck-field-already-declared", "label") - ); - diag.span_label( - self.prev_span, - rustc_errors::DiagnosticMessage::fluent_attr("typeck-field-already-declared", "previous-decl-label") - ); - diag - } -} -``` - -Now that we've defined our diagnostic, how do we [use it][use]? It's quite -straightforward, just create an instance of the struct and pass it to -`emit_err` (or `emit_warning`): - -```rust,ignore -tcx.sess.emit_err(FieldAlreadyDeclared { - field_name: f.ident, - span: f.span, - prev_span, -}); -``` - -## Reference -`#[derive(SessionDiagnostic)]` supports the following attributes: - -- `#[error(code = "...", slug = "...")]` or `#[warning(code = "...", slug = "...")]` - - _Applied to struct._ - - _Mandatory_ - - Defines the struct to be representing an error or a warning. - - `code = "..."` - - _Optional_ - - Specifies the error code. - - `slug = "..."` - - _Mandatory_ - - Uniquely identifies the diagnostic and corresponds to its Fluent message, - mandatory. -- `#[note]` or `#[note = "..."]` - - _Applied to struct or `Span` fields._ - - _Optional_ - - Adds a note sub-diagnostic. - - Value is the Fluent attribute (relative to the Fluent message specified by - `slug`) for the note's message - - Defaults to `note`. - - If applied to a `Span` field, creates a spanned note. -- `#[help]` or `#[help = "..."]` - - _Applied to struct or `Span` fields._ - - _Optional_ - - Adds a help sub-diagnostic. - - Value is the Fluent attribute (relative to the Fluent message specified by - `slug`) for the help's message - - Defaults to `help`. - - If applied to a `Span` field, creates a spanned help. -- `#[label]` or `#[label = "..."]` - - _Applied to `Span` fields._ - - _Optional_ - - Adds a label sub-diagnostic. - - Value is the Fluent attribute (relative to the Fluent message specified by - `slug`) for the label's message - - Defaults to `label`. -- `#[suggestion{,_hidden,_short,_verbose}(message = "...", code = "...")]` - - _Applied to `(Span, MachineApplicability)` or `Span` fields._ - - _Optional_ - - Adds a suggestion sub-diagnostic. - - `message = "..."` - - _Mandatory_ - - Value is the Fluent attribute (relative to the Fluent message specified - by `slug`) for the suggestion's message - - Defaults to `suggestion`. - - `code = "..."` - - _Optional_ - - Value is a format string indicating the code to be suggested as a - replacement. - -[defn]: https://github.com/rust-lang/rust/blob/bbe9d27b8ff36da56638aa43d6d0cdfdf89a4e57/compiler/rustc_typeck/src/errors.rs#L65-L74 -[use]: https://github.com/rust-lang/rust/blob/eb82facb1626166188d49599a3313fc95201f556/compiler/rustc_typeck/src/collect.rs#L981-L985 diff --git a/src/diagnostics/translation.md b/src/diagnostics/translation.md new file mode 100644 index 00000000..5c078ffb --- /dev/null +++ b/src/diagnostics/translation.md @@ -0,0 +1,239 @@ +# Translation +rustc's diagnostic infrastructure supports translatable diagnostics using +[Fluent]. + +## Writing translatable diagnostics +There are two ways of writing translatable diagnostics: + +1. For simple diagnostics, using a diagnostic (or subdiagnostic) derive + ("simple" diagnostics being those that don't require a lot of logic in + deciding to emit subdiagnostics and can therefore be represented as + diagnostic structs). See [the diagnostic and subdiagnostic structs + documentation](./diagnostic-structs.md). +2. Using typed identifiers with `DiagnosticBuilder` APIs (in + `SessionDiagnostic` implementations). + +When adding or changing a translatable diagnostic, you don't need to worry +about the translations, only updating the original English message. Currently, +each crate which defines translatable diagnostics has its own Fluent resource, +such as `parser.ftl` or `typeck.ftl`. + +## Fluent +Fluent is built around the idea of "asymmetric localization", which aims to +decouple the expressiveness of translations from the grammar of the source +language (English in rustc's case). Prior to translation, rustc's diagnostics +relied heavily on interpolation to build the messages shown to the users. +Interpolated strings are hard to translate because writing a natural-sounding +translation might require more, less, or just different interpolation than the +English string, all of which would require changes to the compiler's source +code to support. + +Diagnostic messages are defined in Fluent resources. A combined set of Fluent +resources for a given locale (e.g. `en-US`) is known as Fluent bundle. + +```fluent +typeck-address-of-temporary-taken = cannot take address of a temporary +``` + +In the above example, `typeck-address-of-temporary-taken` is the identifier for +a Fluent message and corresponds to the diagnostic message in English. Other +Fluent resources can be written which would correspond to a message in another +language. Each diagnostic therefore has at least one Fluent message. + +```fluent +typeck-address-of-temporary-taken = cannot take address of a temporary + .label = temporary value +``` + +By convention, diagnostic messages for subdiagnostics are specified as +"attributes" on Fluent messages (additional related messages, denoted by the +`.` syntax). In the above example, `label` is an attribute of +`typeck-address-of-temporary-taken` which corresponds to the message for the +label added to this diagnostic. + +Diagnostic messages often interpolate additional context into the message shown +to the user, such as the name of a type or of a variable. Additional context to +Fluent messages is provided as an "argument" to the diagnostic. + +```fluent +typeck-struct-expr-non-exhaustive = + cannot create non-exhaustive {$what} using struct expression +``` + +In the above example, the Fluent message refers to an argument named `what` +which is expected to exist (how arguments are provided to diagnostics is +discussed in detail later). + +You can consult the [Fluent] documentation for other usage examples of Fluent +and its syntax. + +### Guidelines for writing translatable messages +For a message to be translatable into different languages, all of the +information required by any language must be provided to the diagnostic as an +argument (not just the information required in the English message). + +As the compiler team gain more experience writing diagnostics that have all of +the information necessary to be translated into different languages, this page +will be updated with more guidance. For now, the [Fluent] documentation has +excellent examples of translating messages into different locales and the +information that needs to be provided by the code to do so. + +### Compile-time validation and typed identifiers +rustc's Fluent resources for the default locale (`en-US`) are in the +[`compiler/rustc_error_messages/locales/en-US`] directory. Currently, each crate +which defines translatable diagnostics has its own Fluent resource, such as +`parser.ftl` or `typeck.ftl`. + +rustc's `fluent_messages` macro performs compile-time validation of Fluent +resources and generates code to make it easier to refer to Fluent messages in +diagnostics. + +Compile-time validation of Fluent resources will emit any parsing errors +from Fluent resources while building the compiler, preventing invalid Fluent +resources from causing panics in the compiler. Compile-time validation also +emits an error if multiple Fluent messages have the same identifier. + +In `rustc_error_messages`, `fluent_messages` also generates a constant for each +Fluent message which can be used to refer to messages when emitting +diagnostics and guarantee that the message exists. + +```rust +fluent_messages! { + typeck => "../locales/en-US/typeck.ftl", +} +``` + +For example, given the following Fluent... + +```fluent +typeck-field-multiply-specified-in-initializer = + field `{$ident}` specified more than once + .label = used more than once + .label-previous-use = first use of `{$ident}` +``` + +...then the `fluent_messages` macro will generate: + +```rust +pub static DEFAULT_LOCALE_RESOURCES: &'static [&'static str] = &[ + include_str!("../locales/en-US/typeck.ftl"), +]; + +mod fluent_generated { + mod typeck { + pub const field_multiply_specified_in_initializer: DiagnosticMessage = + DiagnosticMessage::new("typeck-field-multiply-specified-in-initializer"); + pub const label: SubdiagnosticMessage = + SubdiagnosticMessage::attr("label"); + pub const label_previous_use: SubdiagnosticMessage = + SubdiagnosticMessage::attr("previous-use-label"); + } +} +``` + +`rustc_error_messages::fluent_generated` is re-exported and primarily used as +`rustc_errors::fluent`. + +```rust +use rustc_errors::fluent; +let mut err = sess.struct_span_err(span, fluent::typeck::field_multiply_specified_in_initializer); +err.span_label(span, fluent::typeck::label); +err.span_label(previous_use_span, fluent::typeck::previous_use_label); +err.emit(); +``` + +When emitting a diagnostic, these constants can be used like shown above. + +## Internals +Various parts of rustc's diagnostic internals are modified in order to support +translation. + +### Messages +All of rustc's traditional diagnostic APIs (e.g. `struct_span_err` or `note`) +take any message that can be converted into a `DiagnosticMessage` (or +`SubdiagnosticMessage`). + +[`rustc_error_messages::DiagnosticMessage`] can represent legacy non-translatable +diagnostic messages and translatable messages. Non-translatable messages are +just `String`s. Translatable messages are just a `&'static str` with the +identifier of the Fluent message (sometimes with an additional `&'static str` +with an attribute). + +`DiagnosticMessage` never needs to be interacted with directly: +`DiagnosticMessage` constants are created for each diagnostic message in a +Fluent resource (described in more detail below), or `DiagnosticMessage`s will +either be created in the macro-generated code of a diagnostic derive. + +`rustc_error_messages::SubdiagnosticMessage` is similar, it can correspond to a +legacy non-translatable diagnostic message or the name of an attribute to a +Fluent message. Translatable `SubdiagnosticMessage`s must be combined with a +`DiagnosticMessage` (using `DiagnosticMessage::with_subdiagnostic_message`) to +be emitted (an attribute name on its own is meaningless without a corresponding +message identifier, which is what `DiagnosticMessage` provides). + +Both `DiagnosticMessage` and `SubdiagnosticMessage` implement `Into` for any +type that can be converted into a string, and converts these into +non-translatable diagnostics - this keeps all existing diagnostic calls +working. + +### Arguments +Additional context for Fluent messages which are interpolated into message +contents needs to be provided to translatable diagnostics. + +Diagnostics have a `set_arg` function that can be used to provide this +additional context to a diagnostic. + +Arguments have both a name (e.g. "what" in the earlier example) and a value. +Argument values are represented using the `DiagnosticArgValue` type, which is +just a string or a number. rustc types can implement `IntoDiagnosticArg` with +conversion into a string or a number, common types like `Ty<'tcx>` already +have such implementations. + +`set_arg` calls are handled transparently by diagnostic derives but need to be +added manually when using diagnostic builder APIs. + +### Loading +rustc makes a distinction between the "fallback bundle" for `en-US` that is used +by default and when another locale is missing a message; and the primary fluent +bundle which is requested by the user. + +Diagnostic emitters implement the `Emitter` trait which has two functions for +accessing the fallback and primary fluent bundles (`fallback_fluent_bundle` and +`fluent_bundle` respectively). + +`Emitter` also has member functions with default implementations for performing +translation of a `DiagnosticMessage` using the results of +`fallback_fluent_bundle` and `fluent_bundle`. + +All of the emitters in rustc load the fallback Fluent bundle lazily, only +reading Fluent resources and parsing them when an error message is first being +translated (for performance reasons - it doesn't make sense to do this if no +error is being emitted). `rustc_error_messages::fallback_fluent_bundle` returns +a `std::lazy::Lazy` which is provided to emitters and evaluated +in the first call to `Emitter::fallback_fluent_bundle`. + +The primary Fluent bundle (for the user's desired locale) is expected to be +returned by `Emitter::fluent_bundle`. This bundle is used preferentially when +translating messages, the fallback bundle is only used if the primary bundle is +missing a message or not provided. + +As of June 2022, there are no locale bundles +distributed with the compiler, but mechanisms are implemented for loading +bundles. + +- `-Ztranslate-additional-ftl` can be used to load a specific resource as the + primary bundle for testing purposes. +- `-Ztranslate-lang` can be provided a language identifier (something like + `en-US`) and will load any Fluent resources found in + `$sysroot/share/locale/$locale/` directory (both the user provided + sysroot and any sysroot candidates). + +Primary bundles are not currently loaded lazily and if requested will be loaded +at the start of compilation regardless of whether an error occurs. Lazily +loading primary bundles is possible if it can be assumed that loading a bundle +won't fail. Bundle loading can fail if a requested locale is missing, Fluent +files are malformed, or a message is duplicated in multiple resources. + +[Fluent]: https://projectfluent.org +[`compiler/rustc_error_messages/locales/en-US`]: https://github.com/rust-lang/rust/tree/master/compiler/rustc_error_messages/locales/en-US +[`rustc_error_messages::DiagnosticMessage`]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_error_messages/enum.DiagnosticMessage.html