Skip to content

Commit

Permalink
sessiondiagnostic: translation
Browse files Browse the repository at this point in the history
Updates documentation on `#[derive(SessionDiagnostic)]` now that it can
be used for translatable diagnostics.

Signed-off-by: David Wood <[email protected]>
  • Loading branch information
davidtwco committed Apr 4, 2022
1 parent 155126b commit 75edd1e
Showing 1 changed file with 178 additions and 72 deletions.
250 changes: 178 additions & 72 deletions src/diagnostics/sessiondiagnostic.md
Original file line number Diff line number Diff line change
@@ -1,97 +1,203 @@
# 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]() 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.

```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"));
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
}
}
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
```

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.
`typeck-field-already-declared` is the `slug` from our example and is followed
by the diagnostic message.

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.
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.

## Attributes Listing
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.

Below is a listing of all the currently-available attributes that `#[derive(SessionDiagnostic)]`
understands:
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`.

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.
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.

## Optional Diagnostic Attributes
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`.

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:
`#[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).

```rust,ignored
#[derive(SessionDiagnostic)]
#[code = "E0123"]
struct SomeKindOfError {
...
#[suggestion(message = "informative error message")]
opt_sugg: Option<(Span, Applicability)>
...
`#[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 applied to an `Option<Span>` 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]()? 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.

0 comments on commit 75edd1e

Please sign in to comment.