#![deny(unused_must_use)]
use proc_macro::Diagnostic;
use quote::{format_ident, quote};
use syn::spanned::Spanned;
use std::collections::{BTreeSet, HashMap};
pub fn session_diagnostic_derive(s: synstructure::Structure<'_>) -> proc_macro2::TokenStream {
let diag = format_ident!("diag");
let sess = format_ident!("sess");
SessionDiagnosticDerive::new(diag, sess, s).into_tokens()
}
fn type_matches_path(ty: &syn::Type, name: &[&str]) -> bool {
if let syn::Type::Path(ty) = ty {
ty.path
.segments
.iter()
.map(|s| s.ident.to_string())
.rev()
.zip(name.iter().rev())
.all(|(x, y)| &x.as_str() == y)
} else {
false
}
}
struct SessionDiagnosticDerive<'a> {
structure: synstructure::Structure<'a>,
builder: SessionDiagnosticDeriveBuilder<'a>,
}
impl std::convert::From<syn::Error> for SessionDiagnosticDeriveError {
fn from(e: syn::Error) -> Self {
SessionDiagnosticDeriveError::SynError(e)
}
}
enum DiagnosticId {
Error(proc_macro2::TokenStream),
Lint(proc_macro2::TokenStream),
}
#[derive(Debug)]
enum SessionDiagnosticDeriveError {
SynError(syn::Error),
ErrorHandled,
}
impl SessionDiagnosticDeriveError {
fn to_compile_error(self) -> proc_macro2::TokenStream {
match self {
SessionDiagnosticDeriveError::SynError(e) => e.to_compile_error(),
SessionDiagnosticDeriveError::ErrorHandled => {
quote! {
unreachable!()
}
}
}
}
}
fn span_err(span: impl proc_macro::MultiSpan, msg: &str) -> proc_macro::Diagnostic {
Diagnostic::spanned(span, proc_macro::Level::Error, msg)
}
macro_rules! throw_span_err {
($span:expr, $msg:expr) => {{ throw_span_err!($span, $msg, |diag| diag) }};
($span:expr, $msg:expr, $f:expr) => {{
return Err(_throw_span_err($span, $msg, $f));
}};
}
fn _throw_span_err(
span: impl proc_macro::MultiSpan,
msg: &str,
f: impl FnOnce(proc_macro::Diagnostic) -> proc_macro::Diagnostic,
) -> SessionDiagnosticDeriveError {
let diag = span_err(span, msg);
f(diag).emit();
SessionDiagnosticDeriveError::ErrorHandled
}
impl<'a> SessionDiagnosticDerive<'a> {
fn new(diag: syn::Ident, sess: syn::Ident, structure: synstructure::Structure<'a>) -> Self {
let mut fields_map = HashMap::new();
let ast = structure.ast();
if let syn::Data::Struct(syn::DataStruct { fields, .. }) = &ast.data {
for field in fields.iter() {
if let Some(ident) = &field.ident {
fields_map.insert(ident.to_string(), field);
}
}
}
Self {
builder: SessionDiagnosticDeriveBuilder { diag, sess, fields: fields_map, kind: None },
structure,
}
}
fn into_tokens(self) -> proc_macro2::TokenStream {
let SessionDiagnosticDerive { structure, mut builder } = self;
let ast = structure.ast();
let attrs = &ast.attrs;
let implementation = {
if let syn::Data::Struct(..) = ast.data {
let preamble = {
let preamble = attrs.iter().map(|attr| {
builder
.generate_structure_code(attr)
.unwrap_or_else(|v| v.to_compile_error())
});
quote! {
#(#preamble)*;
}
};
let body = structure.each(|field_binding| {
let field = field_binding.ast();
let result = field.attrs.iter().map(|attr| {
builder
.generate_field_code(
attr,
FieldInfo {
vis: &field.vis,
binding: field_binding,
ty: &field.ty,
span: &field.span(),
},
)
.unwrap_or_else(|v| v.to_compile_error())
});
return quote! {
#(#result);*
};
});
match builder.kind {
None => {
span_err(ast.span().unwrap(), "`code` not specified")
.help("use the [code = \"...\"] attribute to set this diagnostic's error code ")
.emit();
SessionDiagnosticDeriveError::ErrorHandled.to_compile_error()
}
Some((kind, _)) => match kind {
DiagnosticId::Lint(_lint) => todo!(),
DiagnosticId::Error(code) => {
let (diag, sess) = (&builder.diag, &builder.sess);
quote! {
let mut #diag = #sess.struct_err_with_code("", rustc_errors::DiagnosticId::Error(#code));
#preamble
match self {
#body
}
#diag
}
}
},
}
} else {
span_err(
ast.span().unwrap(),
"`#[derive(SessionDiagnostic)]` can only be used on structs",
)
.emit();
SessionDiagnosticDeriveError::ErrorHandled.to_compile_error()
}
};
let sess = &builder.sess;
structure.gen_impl(quote! {
gen impl<'__session_diagnostic_sess> rustc_session::SessionDiagnostic<'__session_diagnostic_sess>
for @Self
{
fn into_diagnostic(
self,
#sess: &'__session_diagnostic_sess rustc_session::Session
) -> rustc_errors::DiagnosticBuilder<'__session_diagnostic_sess> {
#implementation
}
}
})
}
}
struct FieldInfo<'a> {
vis: &'a syn::Visibility,
binding: &'a synstructure::BindingInfo<'a>,
ty: &'a syn::Type,
span: &'a proc_macro2::Span,
}
struct SessionDiagnosticDeriveBuilder<'a> {
sess: syn::Ident,
fields: HashMap<String, &'a syn::Field>,
diag: syn::Ident,
kind: Option<(DiagnosticId, proc_macro2::Span)>,
}
impl<'a> SessionDiagnosticDeriveBuilder<'a> {
fn generate_structure_code(
&mut self,
attr: &syn::Attribute,
) -> Result<proc_macro2::TokenStream, SessionDiagnosticDeriveError> {
Ok(match attr.parse_meta()? {
syn::Meta::NameValue(syn::MetaNameValue { lit: syn::Lit::Str(s), .. }) => {
let formatted_str = self.build_format(&s.value(), attr.span());
let name = attr.path.segments.last().unwrap().ident.to_string();
let name = name.as_str();
match name {
"message" => {
let diag = &self.diag;
quote! {
#diag.set_primary_message(#formatted_str);
}
}
attr @ "error" | attr @ "lint" => {
self.set_kind_once(
if attr == "error" {
DiagnosticId::Error(formatted_str)
} else if attr == "lint" {
DiagnosticId::Lint(formatted_str)
} else {
unreachable!()
},
s.span(),
)?;
quote! {}
}
other => throw_span_err!(
attr.span().unwrap(),
&format!(
"`#[{} = ...]` is not a valid SessionDiagnostic struct attribute",
other
)
),
}
}
_ => todo!("unhandled meta kind"),
})
}
#[must_use]
fn set_kind_once(
&mut self,
kind: DiagnosticId,
span: proc_macro2::Span,
) -> Result<(), SessionDiagnosticDeriveError> {
if self.kind.is_none() {
self.kind = Some((kind, span));
Ok(())
} else {
let kind_str = |kind: &DiagnosticId| match kind {
DiagnosticId::Lint(..) => "lint",
DiagnosticId::Error(..) => "error",
};
let existing_kind = kind_str(&self.kind.as_ref().unwrap().0);
let this_kind = kind_str(&kind);
let msg = if this_kind == existing_kind {
format!("`{}` specified multiple times", existing_kind)
} else {
format!("`{}` specified when `{}` was already specified", this_kind, existing_kind)
};
throw_span_err!(span.unwrap(), &msg);
}
}
fn generate_field_code(
&mut self,
attr: &syn::Attribute,
info: FieldInfo<'_>,
) -> Result<proc_macro2::TokenStream, SessionDiagnosticDeriveError> {
let field_binding = &info.binding.binding;
let option_ty = option_inner_ty(&info.ty);
let generated_code = self.generate_non_option_field_code(
attr,
FieldInfo {
vis: info.vis,
binding: info.binding,
ty: option_ty.unwrap_or(&info.ty),
span: info.span,
},
)?;
Ok(if option_ty.is_none() {
quote! { #generated_code }
} else {
quote! {
if let Some(#field_binding) = #field_binding {
#generated_code
}
}
})
}
fn generate_non_option_field_code(
&mut self,
attr: &syn::Attribute,
info: FieldInfo<'_>,
) -> Result<proc_macro2::TokenStream, SessionDiagnosticDeriveError> {
let diag = &self.diag;
let field_binding = &info.binding.binding;
let name = attr.path.segments.last().unwrap().ident.to_string();
let name = name.as_str();
let meta = attr.parse_meta()?;
Ok(match meta {
syn::Meta::NameValue(syn::MetaNameValue { lit: syn::Lit::Str(s), .. }) => {
let formatted_str = self.build_format(&s.value(), attr.span());
match name {
"message" => {
if type_matches_path(&info.ty, &["rustc_span", "Span"]) {
quote! {
#diag.set_span(*#field_binding);
#diag.set_primary_message(#formatted_str);
}
} else {
throw_span_err!(
attr.span().unwrap(),
"the `#[message = \"...\"]` attribute can only be applied to fields of type Span"
);
}
}
"label" => {
if type_matches_path(&info.ty, &["rustc_span", "Span"]) {
quote! {
#diag.span_label(*#field_binding, #formatted_str);
}
} else {
throw_span_err!(
attr.span().unwrap(),
"The `#[label = ...]` attribute can only be applied to fields of type Span"
);
}
}
other => throw_span_err!(
attr.span().unwrap(),
&format!(
"`#[{} = ...]` is not a valid SessionDiagnostic field attribute",
other
)
),
}
}
syn::Meta::List(list) => {
match list.path.segments.iter().last().unwrap().ident.to_string().as_str() {
suggestion_kind @ "suggestion"
| suggestion_kind @ "suggestion_short"
| suggestion_kind @ "suggestion_hidden"
| suggestion_kind @ "suggestion_verbose" => {
let (span, applicability) = (|| match &info.ty {
ty @ syn::Type::Path(..)
if type_matches_path(ty, &["rustc_span", "Span"]) =>
{
let binding = &info.binding.binding;
Ok((
quote!(*#binding),
quote!(rustc_errors::Applicability::Unspecified),
))
}
syn::Type::Tuple(tup) => {
let mut span_idx = None;
let mut applicability_idx = None;
for (idx, elem) in tup.elems.iter().enumerate() {
if type_matches_path(elem, &["rustc_span", "Span"]) {
if span_idx.is_none() {
span_idx = Some(syn::Index::from(idx));
} else {
throw_span_err!(
info.span.clone().unwrap(),
"type of field annotated with `#[suggestion(...)]` contains more than one Span"
);
}
} else if type_matches_path(
elem,
&["rustc_errors", "Applicability"],
) {
if applicability_idx.is_none() {
applicability_idx = Some(syn::Index::from(idx));
} else {
throw_span_err!(
info.span.clone().unwrap(),
"type of field annotated with `#[suggestion(...)]` contains more than one Applicability"
);
}
}
}
if let Some(span_idx) = span_idx {
let binding = &info.binding.binding;
let span = quote!(#binding.#span_idx);
let applicability = applicability_idx
.map(
|applicability_idx| quote!(#binding.#applicability_idx),
)
.unwrap_or_else(|| {
quote!(rustc_errors::Applicability::Unspecified)
});
return Ok((span, applicability));
}
throw_span_err!(
info.span.clone().unwrap(),
"wrong types for suggestion",
|diag| {
diag.help("#[suggestion(...)] on a tuple field must be applied to fields of type (Span, Applicability)")
}
);
}
_ => throw_span_err!(
info.span.clone().unwrap(),
"wrong field type for suggestion",
|diag| {
diag.help("#[suggestion(...)] should be applied to fields of type Span or (Span, Applicability)")
}
),
})()?;
let mut msg = None;
let mut code = None;
for arg in list.nested.iter() {
if let syn::NestedMeta::Meta(syn::Meta::NameValue(arg_name_value)) = arg
{
if let syn::MetaNameValue { lit: syn::Lit::Str(s), .. } =
arg_name_value
{
let name = arg_name_value
.path
.segments
.last()
.unwrap()
.ident
.to_string();
let name = name.as_str();
let formatted_str = self.build_format(&s.value(), arg.span());
match name {
"message" => {
msg = Some(formatted_str);
}
"code" => {
code = Some(formatted_str);
}
other => throw_span_err!(
arg.span().unwrap(),
&format!(
"`{}` is not a valid key for `#[suggestion(...)]`",
other
)
),
}
}
}
}
let msg = if let Some(msg) = msg {
quote!(#msg.as_str())
} else {
throw_span_err!(
list.span().unwrap(),
"missing suggestion message",
|diag| {
diag.help("provide a suggestion message using #[suggestion(message = \"...\")]")
}
);
};
let code = code.unwrap_or_else(|| quote! { String::new() });
let suggestion_method = format_ident!("span_{}", suggestion_kind);
quote! {
#diag.#suggestion_method(#span, #msg, #code, #applicability);
}
}
other => throw_span_err!(
list.span().unwrap(),
&format!("invalid annotation list `#[{}(...)]`", other)
),
}
}
_ => panic!("unhandled meta kind"),
})
}
fn build_format(&self, input: &str, span: proc_macro2::Span) -> proc_macro2::TokenStream {
let mut referenced_fields: BTreeSet<String> = BTreeSet::new();
let mut it = input.chars().peekable();
while let Some(c) = it.next() {
if c == '{' && *it.peek().unwrap_or(&'\0') != '{' {
#[must_use]
let mut eat_argument = || -> Option<String> {
let mut result = String::new();
while let Some(c) = it.next() {
result.push(c);
let next = *it.peek().unwrap_or(&'\0');
if next == '}' {
break;
} else if next == ':' {
assert_eq!(it.next().unwrap(), ':');
break;
}
}
while it.next()? != '}' {
continue;
}
Some(result)
};
if let Some(referenced_field) = eat_argument() {
referenced_fields.insert(referenced_field);
}
}
}
let args = referenced_fields.into_iter().map(|field: String| {
let field_ident = format_ident!("{}", field);
let value = if self.fields.contains_key(&field) {
quote! {
&self.#field_ident
}
} else {
Diagnostic::spanned(
span.unwrap(),
proc_macro::Level::Error,
format!("`{}` doesn't refer to a field on this type", field),
)
.emit();
quote! {
"{#field}"
}
};
quote! {
#field_ident = #value
}
});
quote! {
format!(#input #(,#args)*)
}
}
}
fn option_inner_ty(ty: &syn::Type) -> Option<&syn::Type> {
if type_matches_path(ty, &["std", "option", "Option"]) {
if let syn::Type::Path(ty_path) = ty {
let path = &ty_path.path;
let ty = path.segments.iter().last().unwrap();
if let syn::PathArguments::AngleBracketed(bracketed) = &ty.arguments {
if bracketed.args.len() == 1 {
if let syn::GenericArgument::Type(ty) = &bracketed.args[0] {
return Some(ty);
}
}
}
}
}
None
}