-
Notifications
You must be signed in to change notification settings - Fork 519
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Updates documentation on `#[derive(SessionDiagnostic)]` now that it can be used for translatable diagnostics. Signed-off-by: David Wood <[email protected]>
- Loading branch information
Showing
1 changed file
with
178 additions
and
72 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |