Creating Errors With SessionDiagnostic

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.

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 and usage here):

#[derive(SessionDiagnostic)]
#[error = "E0124"]
pub struct FieldAlreadyDeclared {
    pub field_name: Ident,
    #[message = "field `{field_name}` is already declared"]
    #[label = "field already declared"]
    pub span: Span,
    #[label = "`{field_name}` first declared here"]
    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):

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

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:

AttributeApplied toMandatoryBehaviour
#[code = "..."]StructYesSets the Diagnostic's error code
#[message = "..."]Struct / Span fieldsYesSets the Diagnostic's primary message. If on Span field, also sets the Diagnostic's span.
#[label = "..."]Span fieldsNoEquivalent to calling span_label with that Span and message.
#[suggestion(message = "..." , code = "..."](Span, MachineApplicability) or Span fieldsNoEquivalent to calling span_suggestion. Note code is optional.
#[suggestion_short(message = "..." , code = "..."](Span, MachineApplicability) or Span fieldsNoEquivalent to calling span_suggestion_short. Note code is optional.
#[suggestion_hidden(message = "..." , code = "..."](Span, MachineApplicability) or Span fieldsNoEquivalent to calling span_suggestion_hidden. Note code is optional.
#[suggestion_verbose(message = "..." , code = "..."](Span, MachineApplicability) or Span fieldsNoEquivalent to calling span_suggestion_verbose. Note code is optional.

Optional Diagnostic 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:

#[derive(SessionDiagnostic)]
#[code = "E0123"]
struct SomeKindOfError {
    ...
    #[suggestion(message = "informative error message")]
    opt_sugg: Option<(Span, Applicability)>
    ...
}