Skip to content

Commit

Permalink
macros: separate suggestion fmt'ing and emission
Browse files Browse the repository at this point in the history
Diagnostic derives have previously had to take special care when
ordering the generated code so that fields were not used after a move.

This is unlikely for most fields because a field is either annotated
with a subdiagnostic attribute and is thus likely a `Span` and copiable,
or is a argument, in which case it is only used once by `set_arg`
anyway.

However, format strings for code in suggestions can result in fields
being used after being moved if not ordered carefully. As a result, the
derive currently puts `set_arg` calls last (just before emission), such
as:

```rust
let diag = { /* create diagnostic */ };

diag.span_suggestion_with_style(
    span,
    fluent::crate::slug,
    format!("{}", __binding_0),
    Applicability::Unknown,
    SuggestionStyle::ShowAlways
);
/* + other subdiagnostic additions */

diag.set_arg("foo", __binding_0);
/* + other `set_arg` calls */

diag.emit();
```

For eager translation, this doesn't work, as the message being
translated eagerly can assume that all arguments are available - so
arguments _must_ be set first.

Format strings for suggestion code are now separated into two parts - an
initialization line that performs the formatting into a variable, and a
usage in the subdiagnostic addition.

By separating these parts, the initialization can happen before
arguments are set, preserving the desired order so that code compiles,
while still enabling arguments to be set before subdiagnostics are
added.

```rust
let diag = { /* create diagnostic */ };

let __code_0 = format!("{}", __binding_0);
/* + other formatting */

diag.set_arg("foo", __binding_0);
/* + other `set_arg` calls */

diag.span_suggestion_with_style(
    span,
    fluent::crate::slug,
    __code_0,
    Applicability::Unknown,
    SuggestionStyle::ShowAlways
);
/* + other subdiagnostic additions */

diag.emit();
```

Signed-off-by: David Wood <david.wood@huawei.com>
  • Loading branch information
davidtwco committed Oct 10, 2022
1 parent 113e943 commit 7e20929
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 23 deletions.
6 changes: 4 additions & 2 deletions compiler/rustc_macros/src/diagnostics/diagnostic_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,8 @@ impl<'a> DiagnosticDeriveVariantBuilder<'a> {
SubdiagnosticKind::Suggestion {
suggestion_kind,
applicability: static_applicability,
code,
code_field,
code_init,
} => {
let (span_field, mut applicability) = self.span_and_applicability_of_ty(info)?;

Expand All @@ -440,10 +441,11 @@ impl<'a> DiagnosticDeriveVariantBuilder<'a> {
let style = suggestion_kind.to_suggestion_style();

Ok(quote! {
#code_init
#diag.span_suggestion_with_style(
#span_field,
rustc_errors::fluent::#slug,
#code,
#code_field,
#applicability,
#style
);
Expand Down
50 changes: 38 additions & 12 deletions compiler/rustc_macros/src/diagnostics/subdiagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::diagnostics::error::{
DiagnosticDeriveError,
};
use crate::diagnostics::utils::{
build_field_mapping, report_error_if_not_applied_to_applicability,
build_field_mapping, new_code_ident, report_error_if_not_applied_to_applicability,
report_error_if_not_applied_to_span, FieldInfo, FieldInnerTy, FieldMap, HasFieldMap, SetOnce,
SpannedOption, SubdiagnosticKind,
};
Expand Down Expand Up @@ -57,6 +57,7 @@ impl SubdiagnosticDeriveBuilder {
parent: &self,
variant,
span,
formatting_init: TokenStream::new(),
fields: build_field_mapping(variant),
span_field: None,
applicability: None,
Expand Down Expand Up @@ -105,6 +106,9 @@ struct SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
/// Span for the entire type.
span: proc_macro::Span,

/// Initialization of format strings for code suggestions.
formatting_init: TokenStream,

/// Store a map of field name to its corresponding field. This is built on construction of the
/// derive builder.
fields: FieldMap,
Expand Down Expand Up @@ -230,7 +234,7 @@ impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
};

let generated = self
.generate_field_code_inner(kind_stats, attr, info)
.generate_field_code_inner(kind_stats, attr, info, inner_ty.will_iterate())
.unwrap_or_else(|v| v.to_compile_error());

inner_ty.with(binding, generated)
Expand All @@ -243,13 +247,18 @@ impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
kind_stats: KindsStatistics,
attr: &Attribute,
info: FieldInfo<'_>,
clone_suggestion_code: bool,
) -> Result<TokenStream, DiagnosticDeriveError> {
let meta = attr.parse_meta()?;
match meta {
Meta::Path(path) => self.generate_field_code_inner_path(kind_stats, attr, info, path),
Meta::List(list @ MetaList { .. }) => {
self.generate_field_code_inner_list(kind_stats, attr, info, list)
}
Meta::List(list @ MetaList { .. }) => self.generate_field_code_inner_list(
kind_stats,
attr,
info,
list,
clone_suggestion_code,
),
_ => throw_invalid_attr!(attr, &meta),
}
}
Expand Down Expand Up @@ -353,6 +362,7 @@ impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
attr: &Attribute,
info: FieldInfo<'_>,
list: MetaList,
clone_suggestion_code: bool,
) -> Result<TokenStream, DiagnosticDeriveError> {
let span = attr.span().unwrap();
let ident = &list.path.segments.last().unwrap().ident;
Expand Down Expand Up @@ -390,22 +400,29 @@ impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
match nested_name {
"code" => {
let formatted_str = self.build_format(&value.value(), value.span());
code.set_once(formatted_str, span);
let code_field = new_code_ident();
code.set_once((code_field, formatted_str), span);
}
_ => throw_invalid_nested_attr!(attr, &nested_attr, |diag| {
diag.help("`code` is the only valid nested attribute")
}),
}
}

let Some((code, _)) = code else {
let Some((code_field, formatted_str)) = code.value() else {
span_err(span, "`#[suggestion_part(...)]` attribute without `code = \"...\"`")
.emit();
return Ok(quote! {});
};
let binding = info.binding;

Ok(quote! { suggestions.push((#binding, #code)); })
self.formatting_init.extend(quote! { let #code_field = #formatted_str; });
let code_field = if clone_suggestion_code {
quote! { #code_field.clone() }
} else {
quote! { #code_field }
};
Ok(quote! { suggestions.push((#binding, #code_field)); })
}
_ => throw_invalid_attr!(attr, &Meta::List(list), |diag| {
let mut span_attrs = vec![];
Expand Down Expand Up @@ -459,7 +476,14 @@ impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {

let name = format_ident!("{}{}", if span_field.is_some() { "span_" } else { "" }, kind);
let call = match kind {
SubdiagnosticKind::Suggestion { suggestion_kind, applicability, code } => {
SubdiagnosticKind::Suggestion {
suggestion_kind,
applicability,
code_init,
code_field,
} => {
self.formatting_init.extend(code_init);

let applicability = applicability
.value()
.map(|a| quote! { #a })
Expand All @@ -468,8 +492,7 @@ impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {

if let Some(span) = span_field {
let style = suggestion_kind.to_suggestion_style();

quote! { #diag.#name(#span, #message, #code, #applicability, #style); }
quote! { #diag.#name(#span, #message, #code_field, #applicability, #style); }
} else {
span_err(self.span, "suggestion without `#[primary_span]` field").emit();
quote! { unreachable!(); }
Expand Down Expand Up @@ -510,6 +533,7 @@ impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
}
}
};

calls.extend(call);
}

Expand All @@ -521,11 +545,13 @@ impl<'parent, 'a> SubdiagnosticDeriveVariantBuilder<'parent, 'a> {
.map(|binding| self.generate_field_set_arg(binding))
.collect();

let formatting_init = &self.formatting_init;
Ok(quote! {
#init
#formatting_init
#attr_args
#calls
#plain_args
#calls
})
}
}
48 changes: 39 additions & 9 deletions compiler/rustc_macros/src/diagnostics/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::diagnostics::error::{
use proc_macro::Span;
use proc_macro2::TokenStream;
use quote::{format_ident, quote, ToTokens};
use std::cell::RefCell;
use std::cmp::Ordering;
use std::collections::{BTreeSet, HashMap};
use std::fmt;
Expand All @@ -14,6 +15,19 @@ use synstructure::{BindStyle, BindingInfo, VariantInfo};

use super::error::invalid_nested_attr;

thread_local! {
pub static CODE_IDENT_COUNT: RefCell<u32> = RefCell::new(0);
}

/// Returns an ident of the form `__code_N` where `N` is incremented once with every call.
pub(crate) fn new_code_ident() -> syn::Ident {
CODE_IDENT_COUNT.with(|count| {
let ident = format_ident!("__code_{}", *count.borrow());
*count.borrow_mut() += 1;
ident
})
}

/// Checks whether the type name of `ty` matches `name`.
///
/// Given some struct at `a::b::c::Foo`, this will return true for `c::Foo`, `b::c::Foo`, or
Expand Down Expand Up @@ -142,6 +156,15 @@ impl<'ty> FieldInnerTy<'ty> {
unreachable!();
}

/// Returns `true` if `FieldInnerTy::with` will result in iteration for this inner type (i.e.
/// that cloning might be required for values moved in the loop body).
pub(crate) fn will_iterate(&self) -> bool {
match self {
FieldInnerTy::Vec(..) => true,
FieldInnerTy::Option(..) | FieldInnerTy::None => false,
}
}

/// Returns `Option` containing inner type if there is one.
pub(crate) fn inner_type(&self) -> Option<&'ty Type> {
match self {
Expand Down Expand Up @@ -434,7 +457,12 @@ pub(super) enum SubdiagnosticKind {
Suggestion {
suggestion_kind: SuggestionKind,
applicability: SpannedOption<Applicability>,
code: TokenStream,
/// Identifier for variable used for formatted code, e.g. `___code_0`. Enables separation
/// of formatting and diagnostic emission so that `set_arg` calls can happen in-between..
code_field: syn::Ident,
/// Initialization logic for `code_field`'s variable, e.g.
/// `let __formatted_code = /* whatever */;`
code_init: TokenStream,
},
/// `#[multipart_suggestion{,_short,_hidden,_verbose}]`
MultipartSuggestion {
Expand Down Expand Up @@ -469,7 +497,8 @@ impl SubdiagnosticKind {
SubdiagnosticKind::Suggestion {
suggestion_kind,
applicability: None,
code: TokenStream::new(),
code_field: new_code_ident(),
code_init: TokenStream::new(),
}
} else if let Some(suggestion_kind) =
name.strip_prefix("multipart_suggestion").and_then(|s| s.parse().ok())
Expand Down Expand Up @@ -548,9 +577,10 @@ impl SubdiagnosticKind {
};

match (nested_name, &mut kind) {
("code", SubdiagnosticKind::Suggestion { .. }) => {
("code", SubdiagnosticKind::Suggestion { code_field, .. }) => {
let formatted_str = fields.build_format(&value.value(), value.span());
code.set_once(formatted_str, span);
let code_init = quote! { let #code_field = #formatted_str; };
code.set_once(code_init, span);
}
(
"applicability",
Expand Down Expand Up @@ -582,13 +612,13 @@ impl SubdiagnosticKind {
}

match kind {
SubdiagnosticKind::Suggestion { code: ref mut code_field, .. } => {
*code_field = if let Some((code, _)) = code {
code
SubdiagnosticKind::Suggestion { ref code_field, ref mut code_init, .. } => {
*code_init = if let Some(init) = code.value() {
init
} else {
span_err(span, "suggestion without `code = \"...\"`").emit();
quote! { "" }
}
quote! { let #code_field: String = unreachable!(); }
};
}
SubdiagnosticKind::Label
| SubdiagnosticKind::Note
Expand Down
24 changes: 24 additions & 0 deletions src/test/ui-fulldeps/session-diagnostic/diagnostic-derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -725,3 +725,27 @@ struct SubdiagnosticEagerCorrect {
#[subdiagnostic(eager)]
note: Note,
}

// Check that formatting of `correct` in suggestion doesn't move the binding for that field, making
// the `set_arg` call a compile error; and that isn't worked around by moving the `set_arg` call
// after the `span_suggestion` call - which breaks eager translation.

#[derive(Subdiagnostic)]
#[suggestion_short(
parser::use_instead,
applicability = "machine-applicable",
code = "{correct}"
)]
pub(crate) struct SubdiagnosticWithSuggestion {
#[primary_span]
span: Span,
invalid: String,
correct: String,
}

#[derive(Diagnostic)]
#[diag(compiletest::example)]
struct SubdiagnosticEagerSuggestion {
#[subdiagnostic(eager)]
sub: SubdiagnosticWithSuggestion,
}

0 comments on commit 7e20929

Please sign in to comment.