From 106c304e512187999de47311bf392e49bb30668d Mon Sep 17 00:00:00 2001 From: David Wood Date: Mon, 4 Apr 2022 06:15:30 +0100 Subject: [PATCH] sessiondiagnostic: translation Updates documentation on `#[derive(SessionDiagnostic)]` now that it can be used for translatable diagnostics. Signed-off-by: David Wood --- src/diagnostics/sessiondiagnostic.md | 252 +++++++++++++++++++-------- 1 file changed, 181 insertions(+), 71 deletions(-) diff --git a/src/diagnostics/sessiondiagnostic.md b/src/diagnostics/sessiondiagnostic.md index 4583e6d3d..e82d5629d 100644 --- a/src/diagnostics/sessiondiagnostic.md +++ b/src/diagnostics/sessiondiagnostic.md @@ -1,97 +1,207 @@ -# Creating Errors With SessionDiagnostic +# 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. -The SessionDiagnostic derive macro gives an alternate way to the DiagnosticBuilder API for defining -and emitting errors. It allows a struct to be annotated with information which allows it to be -transformed and emitted as a Diagnostic. +Instead of using the `DiagnosticBuilder` API to create and emit diagnostics, +the `SessionDiagnostic` derive macro is applied to structs. -As an example, we'll take a look at how the "field already declared" diagnostic is actually defined -in the compiler (see the definition -[here](https://github.com/rust-lang/rust/blob/75042566d1c90d912f22e4db43b6d3af98447986/compiler/rustc_typeck/src/errors.rs#L65-L74) -and usage -[here](https://github.com/rust-lang/rust/blob/75042566d1c90d912f22e4db43b6d3af98447986/compiler/rustc_typeck/src/collect.rs#L863-L867)): +The [definition][defn] of the "field already declared" diagnostic is shown +below. ```rust,ignore #[derive(SessionDiagnostic)] -#[error = "E0124"] +#[error(code = "E0124", slug = "typeck-field-already-declared")] pub struct FieldAlreadyDeclared { pub field_name: Ident, - #[message = "field `{field_name}` is already declared"] - #[label = "field already declared"] + #[primary_span] + #[label] pub span: Span, - #[label = "`{field_name}` first declared here"] + #[label = "previous-decl-label"] pub prev_span: Span, } -// ... -tcx.sess.emit_err(FieldAlreadyDeclared { - field_name: f.ident, - span: f.span, - prev_span, -}); ``` -We see that using `SessionDiagnostic` is relatively straight forward. The `#[error = "..."]` -attribute is used to supply the error code for the diagnostic. We then annotate fields in the -struct with various information on how to convert an instance of the struct into a rendered -diagnostic. The attributes above produce code which is roughly equivalent to the following (in -pseudo-Rust): +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::DiagnosticId::Error("E0124")); + 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.set_primary_message(format!("field `{field_name}` is already declared", field_name = self.field_name)); - diag.span_label(self.span, "field already declared"); - diag.span_label(self.prev_span, format!("`{field_name}` first declared here", field_name = self.field_name)); + 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 } } ``` -The generated code draws attention to a number of features. First, we see that within the strings -passed to each attribute, field names can be referenced without needing to be passed -explicitly into the format string -- in this example here, `#[message = "field {field_name} is -already declared"]` produces a call to `format!` with the appropriate arguments to format -`self.field_name` into the string. This applies to strings passed to all attributes. - -We also see that labelling `Span` fields in the struct produces calls which pass that `Span` to the -produced diagnostic. In the example above, we see that putting the `#[message = "..."]` attribute -on a `Span` leads to the primary span of the diagnostic being set to that `Span`, while applying the -`#[label = "..."]` attribute on a Span will simply set the span for that label. -Each attribute has different requirements for what they can be applied on, differing on position -(on the struct, or on a specific field), type (if it's applied on a field), and whether or not the -attribute is optional. - -## Attributes Listing - -Below is a listing of all the currently-available attributes that `#[derive(SessionDiagnostic)]` -understands: - -Attribute | Applied to | Mandatory | Behaviour -:-------------- | :-------------------- |:--------- | :--------- -`#[code = "..."]` | Struct | Yes | Sets the Diagnostic's error code -`#[message = "..."]` | Struct / `Span` fields | Yes | Sets the Diagnostic's primary message. If on `Span` field, also sets the Diagnostic's span. -`#[label = "..."]` | `Span` fields | No | Equivalent to calling `span_label` with that Span and message. -`#[suggestion(message = "..." , code = "..."]` | `(Span, MachineApplicability)` or `Span` fields | No | Equivalent to calling `span_suggestion`. Note `code` is optional. -`#[suggestion_short(message = "..." , code = "..."]` | `(Span, MachineApplicability)` or `Span` fields | No | Equivalent to calling `span_suggestion_short`. Note `code` is optional. -`#[suggestion_hidden(message = "..." , code = "..."]` | `(Span, MachineApplicability)` or `Span` fields | No | Equivalent to calling `span_suggestion_hidden`. Note `code` is optional. -`#[suggestion_verbose(message = "..." , code = "..."]` | `(Span, MachineApplicability)` or `Span` fields | No | Equivalent to calling `span_suggestion_verbose`. Note `code` is optional. +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, +}); +``` -## Optional Diagnostic Attributes +## Reference +`#[derive(SessionDiagnostic)]` supports the following attributes: -There may be some cases where you want one of the decoration attributes to be applied optionally; -for example, if a suggestion can only be generated sometimes. In this case, simply wrap the field's -type in an `Option`. At runtime, if the field is set to `None`, the attribute for that field won't -be used in creating the diagnostic. For example: +- `#[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. -```rust,ignored -#[derive(SessionDiagnostic)] -#[code = "E0123"] -struct SomeKindOfError { - ... - #[suggestion(message = "informative error message")] - opt_sugg: Option<(Span, Applicability)> - ... -} -``` +[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