use std::cell::RefCell;
use std::collections::{BTreeSet, HashMap};
use std::fmt;
use std::str::FromStr;
use proc_macro::Span;
use proc_macro2::{Ident, TokenStream};
use quote::{ToTokens, format_ident, quote};
use syn::meta::ParseNestedMeta;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{Attribute, Field, LitStr, Meta, Path, Token, Type, TypeTuple, parenthesized};
use synstructure::{BindingInfo, VariantInfo};
use super::error::invalid_attr;
use crate::diagnostics::error::{
DiagnosticDeriveError, span_err, throw_invalid_attr, throw_span_err,
};
thread_local! {
pub(crate) static CODE_IDENT_COUNT: RefCell<u32> = RefCell::new(0);
}
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
})
}
pub(crate) fn type_matches_path(ty: &Type, name: &[&str]) -> bool {
if let 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
}
}
pub(crate) fn type_is_unit(ty: &Type) -> bool {
if let Type::Tuple(TypeTuple { elems, .. }) = ty { elems.is_empty() } else { false }
}
pub(crate) fn type_is_bool(ty: &Type) -> bool {
type_matches_path(ty, &["bool"])
}
pub(crate) fn report_type_error(
attr: &Attribute,
ty_name: &str,
) -> Result<!, DiagnosticDeriveError> {
let name = attr.path().segments.last().unwrap().ident.to_string();
let meta = &attr.meta;
throw_span_err!(
attr.span().unwrap(),
&format!(
"the `#[{}{}]` attribute can only be applied to fields of type {}",
name,
match meta {
Meta::Path(_) => "",
Meta::NameValue(_) => " = ...",
Meta::List(_) => "(...)",
},
ty_name
)
);
}
fn report_error_if_not_applied_to_ty(
attr: &Attribute,
info: &FieldInfo<'_>,
path: &[&str],
ty_name: &str,
) -> Result<(), DiagnosticDeriveError> {
if !type_matches_path(info.ty.inner_type(), path) {
report_type_error(attr, ty_name)?;
}
Ok(())
}
pub(crate) fn report_error_if_not_applied_to_applicability(
attr: &Attribute,
info: &FieldInfo<'_>,
) -> Result<(), DiagnosticDeriveError> {
report_error_if_not_applied_to_ty(
attr,
info,
&["rustc_errors", "Applicability"],
"`Applicability`",
)
}
pub(crate) fn report_error_if_not_applied_to_span(
attr: &Attribute,
info: &FieldInfo<'_>,
) -> Result<(), DiagnosticDeriveError> {
if !type_matches_path(info.ty.inner_type(), &["rustc_span", "Span"])
&& !type_matches_path(info.ty.inner_type(), &["rustc_errors", "MultiSpan"])
{
report_type_error(attr, "`Span` or `MultiSpan`")?;
}
Ok(())
}
#[derive(Copy, Clone)]
pub(crate) enum FieldInnerTy<'ty> {
Option(&'ty Type),
Vec(&'ty Type),
Plain(&'ty Type),
}
impl<'ty> FieldInnerTy<'ty> {
pub(crate) fn from_type(ty: &'ty Type) -> Self {
fn single_generic_type(ty: &Type) -> &Type {
let Type::Path(ty_path) = ty else {
panic!("expected path type");
};
let path = &ty_path.path;
let ty = path.segments.iter().last().unwrap();
let syn::PathArguments::AngleBracketed(bracketed) = &ty.arguments else {
panic!("expected bracketed generic arguments");
};
assert_eq!(bracketed.args.len(), 1);
let syn::GenericArgument::Type(ty) = &bracketed.args[0] else {
panic!("expected generic parameter to be a type generic");
};
ty
}
if type_matches_path(ty, &["std", "option", "Option"]) {
FieldInnerTy::Option(single_generic_type(ty))
} else if type_matches_path(ty, &["std", "vec", "Vec"]) {
FieldInnerTy::Vec(single_generic_type(ty))
} else {
FieldInnerTy::Plain(ty)
}
}
pub(crate) fn will_iterate(&self) -> bool {
match self {
FieldInnerTy::Vec(..) => true,
FieldInnerTy::Option(..) | FieldInnerTy::Plain(_) => false,
}
}
pub(crate) fn inner_type(&self) -> &'ty Type {
match self {
FieldInnerTy::Option(inner) | FieldInnerTy::Vec(inner) | FieldInnerTy::Plain(inner) => {
inner
}
}
}
pub(crate) fn with(&self, binding: impl ToTokens, inner: impl ToTokens) -> TokenStream {
match self {
FieldInnerTy::Option(..) => quote! {
if let Some(#binding) = #binding {
#inner
}
},
FieldInnerTy::Vec(..) => quote! {
for #binding in #binding {
#inner
}
},
FieldInnerTy::Plain(t) if type_is_bool(t) => quote! {
if #binding {
#inner
}
},
FieldInnerTy::Plain(..) => quote! { #inner },
}
}
pub(crate) fn span(&self) -> proc_macro2::Span {
match self {
FieldInnerTy::Option(ty) | FieldInnerTy::Vec(ty) | FieldInnerTy::Plain(ty) => ty.span(),
}
}
}
pub(crate) struct FieldInfo<'a> {
pub(crate) binding: &'a BindingInfo<'a>,
pub(crate) ty: FieldInnerTy<'a>,
pub(crate) span: &'a proc_macro2::Span,
}
pub(crate) trait SetOnce<T> {
fn set_once(&mut self, value: T, span: Span);
fn value(self) -> Option<T>;
fn value_ref(&self) -> Option<&T>;
}
pub(super) type SpannedOption<T> = Option<(T, Span)>;
impl<T> SetOnce<T> for SpannedOption<T> {
fn set_once(&mut self, value: T, span: Span) {
match self {
None => {
*self = Some((value, span));
}
Some((_, prev_span)) => {
span_err(span, "specified multiple times")
.span_note(*prev_span, "previously specified here")
.emit();
}
}
}
fn value(self) -> Option<T> {
self.map(|(v, _)| v)
}
fn value_ref(&self) -> Option<&T> {
self.as_ref().map(|(v, _)| v)
}
}
pub(super) type FieldMap = HashMap<String, TokenStream>;
pub(crate) trait HasFieldMap {
fn get_field_binding(&self, field: &String) -> Option<&TokenStream>;
fn build_format(&self, input: &str, span: proc_macro2::Span) -> TokenStream {
let mut referenced_fields: BTreeSet<String> = BTreeSet::new();
let mut it = input.chars().peekable();
while let Some(c) = it.next() {
if c != '{' {
continue;
}
if *it.peek().unwrap_or(&'\0') == '{' {
assert_eq!(it.next().unwrap(), '{');
continue;
}
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 = match self.get_field_binding(&field) {
Some(value) => value.clone(),
None => {
span_err(
span.unwrap(),
format!("`{field}` doesn't refer to a field on this type"),
)
.emit();
quote! {
"{#field}"
}
}
};
quote! {
#field_ident = #value
}
});
quote! {
format!(#input #(,#args)*)
}
}
}
#[derive(Clone, Copy)]
pub(crate) enum Applicability {
MachineApplicable,
MaybeIncorrect,
HasPlaceholders,
Unspecified,
}
impl FromStr for Applicability {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"machine-applicable" => Ok(Applicability::MachineApplicable),
"maybe-incorrect" => Ok(Applicability::MaybeIncorrect),
"has-placeholders" => Ok(Applicability::HasPlaceholders),
"unspecified" => Ok(Applicability::Unspecified),
_ => Err(()),
}
}
}
impl quote::ToTokens for Applicability {
fn to_tokens(&self, tokens: &mut TokenStream) {
tokens.extend(match self {
Applicability::MachineApplicable => {
quote! { rustc_errors::Applicability::MachineApplicable }
}
Applicability::MaybeIncorrect => {
quote! { rustc_errors::Applicability::MaybeIncorrect }
}
Applicability::HasPlaceholders => {
quote! { rustc_errors::Applicability::HasPlaceholders }
}
Applicability::Unspecified => {
quote! { rustc_errors::Applicability::Unspecified }
}
});
}
}
pub(super) fn build_field_mapping(variant: &VariantInfo<'_>) -> HashMap<String, TokenStream> {
let mut fields_map = FieldMap::new();
for binding in variant.bindings() {
if let Some(ident) = &binding.ast().ident {
fields_map.insert(ident.to_string(), quote! { #binding });
}
}
fields_map
}
#[derive(Copy, Clone, Debug)]
pub(super) enum AllowMultipleAlternatives {
No,
Yes,
}
fn parse_suggestion_values(
nested: ParseNestedMeta<'_>,
allow_multiple: AllowMultipleAlternatives,
) -> syn::Result<Vec<LitStr>> {
let values = if let Ok(val) = nested.value() {
vec![val.parse()?]
} else {
let content;
parenthesized!(content in nested.input);
if let AllowMultipleAlternatives::No = allow_multiple {
span_err(
nested.input.span().unwrap(),
"expected exactly one string literal for `code = ...`",
)
.emit();
vec![]
} else {
let literals = Punctuated::<LitStr, Token![,]>::parse_terminated(&content);
match literals {
Ok(p) if p.is_empty() => {
span_err(
content.span().unwrap(),
"expected at least one string literal for `code(...)`",
)
.emit();
vec![]
}
Ok(p) => p.into_iter().collect(),
Err(_) => {
span_err(
content.span().unwrap(),
"`code(...)` must contain only string literals",
)
.emit();
vec![]
}
}
}
};
Ok(values)
}
pub(super) fn build_suggestion_code(
code_field: &Ident,
nested: ParseNestedMeta<'_>,
fields: &impl HasFieldMap,
allow_multiple: AllowMultipleAlternatives,
) -> TokenStream {
let values = match parse_suggestion_values(nested, allow_multiple) {
Ok(x) => x,
Err(e) => return e.into_compile_error(),
};
if let AllowMultipleAlternatives::Yes = allow_multiple {
let formatted_strings: Vec<_> = values
.into_iter()
.map(|value| fields.build_format(&value.value(), value.span()))
.collect();
quote! { let #code_field = [#(#formatted_strings),*].into_iter(); }
} else if let [value] = values.as_slice() {
let formatted_str = fields.build_format(&value.value(), value.span());
quote! { let #code_field = #formatted_str; }
} else {
quote! { let #code_field = String::new(); }
}
}
#[derive(Clone, Copy, PartialEq)]
pub(super) enum SuggestionKind {
Normal,
Short,
Hidden,
Verbose,
ToolOnly,
}
impl FromStr for SuggestionKind {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"normal" => Ok(SuggestionKind::Normal),
"short" => Ok(SuggestionKind::Short),
"hidden" => Ok(SuggestionKind::Hidden),
"verbose" => Ok(SuggestionKind::Verbose),
"tool-only" => Ok(SuggestionKind::ToolOnly),
_ => Err(()),
}
}
}
impl fmt::Display for SuggestionKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SuggestionKind::Normal => write!(f, "normal"),
SuggestionKind::Short => write!(f, "short"),
SuggestionKind::Hidden => write!(f, "hidden"),
SuggestionKind::Verbose => write!(f, "verbose"),
SuggestionKind::ToolOnly => write!(f, "tool-only"),
}
}
}
impl SuggestionKind {
pub(crate) fn to_suggestion_style(&self) -> TokenStream {
match self {
SuggestionKind::Normal => {
quote! { rustc_errors::SuggestionStyle::ShowCode }
}
SuggestionKind::Short => {
quote! { rustc_errors::SuggestionStyle::HideCodeInline }
}
SuggestionKind::Hidden => {
quote! { rustc_errors::SuggestionStyle::HideCodeAlways }
}
SuggestionKind::Verbose => {
quote! { rustc_errors::SuggestionStyle::ShowAlways }
}
SuggestionKind::ToolOnly => {
quote! { rustc_errors::SuggestionStyle::CompletelyHidden }
}
}
}
fn from_suffix(s: &str) -> Option<Self> {
match s {
"" => Some(SuggestionKind::Normal),
"_short" => Some(SuggestionKind::Short),
"_hidden" => Some(SuggestionKind::Hidden),
"_verbose" => Some(SuggestionKind::Verbose),
_ => None,
}
}
}
#[derive(Clone)]
pub(super) enum SubdiagnosticKind {
Label,
Note,
NoteOnce,
Help,
HelpOnce,
Warn,
Suggestion {
suggestion_kind: SuggestionKind,
applicability: SpannedOption<Applicability>,
code_field: syn::Ident,
code_init: TokenStream,
},
MultipartSuggestion {
suggestion_kind: SuggestionKind,
applicability: SpannedOption<Applicability>,
},
}
pub(super) struct SubdiagnosticVariant {
pub(super) kind: SubdiagnosticKind,
pub(super) slug: Option<Path>,
pub(super) no_span: bool,
}
impl SubdiagnosticVariant {
pub(super) fn from_attr(
attr: &Attribute,
fields: &impl HasFieldMap,
) -> Result<Option<SubdiagnosticVariant>, DiagnosticDeriveError> {
if is_doc_comment(attr) {
return Ok(None);
}
let span = attr.span().unwrap();
let name = attr.path().segments.last().unwrap().ident.to_string();
let name = name.as_str();
let mut kind = match name {
"label" => SubdiagnosticKind::Label,
"note" => SubdiagnosticKind::Note,
"note_once" => SubdiagnosticKind::NoteOnce,
"help" => SubdiagnosticKind::Help,
"help_once" => SubdiagnosticKind::HelpOnce,
"warning" => SubdiagnosticKind::Warn,
_ => {
if let Some(suggestion_kind) =
name.strip_prefix("suggestion").and_then(SuggestionKind::from_suffix)
{
if suggestion_kind != SuggestionKind::Normal {
invalid_attr(attr)
.help(format!(
r#"Use `#[suggestion(..., style = "{suggestion_kind}")]` instead"#
))
.emit();
}
SubdiagnosticKind::Suggestion {
suggestion_kind: SuggestionKind::Normal,
applicability: None,
code_field: new_code_ident(),
code_init: TokenStream::new(),
}
} else if let Some(suggestion_kind) =
name.strip_prefix("multipart_suggestion").and_then(SuggestionKind::from_suffix)
{
if suggestion_kind != SuggestionKind::Normal {
invalid_attr(attr)
.help(format!(
r#"Use `#[multipart_suggestion(..., style = "{suggestion_kind}")]` instead"#
))
.emit();
}
SubdiagnosticKind::MultipartSuggestion {
suggestion_kind: SuggestionKind::Normal,
applicability: None,
}
} else {
throw_invalid_attr!(attr);
}
}
};
let list = match &attr.meta {
Meta::List(list) => {
list
}
Meta::Path(_) => {
match kind {
SubdiagnosticKind::Label
| SubdiagnosticKind::Note
| SubdiagnosticKind::NoteOnce
| SubdiagnosticKind::Help
| SubdiagnosticKind::HelpOnce
| SubdiagnosticKind::Warn
| SubdiagnosticKind::MultipartSuggestion { .. } => {
return Ok(Some(SubdiagnosticVariant { kind, slug: None, no_span: false }));
}
SubdiagnosticKind::Suggestion { .. } => {
throw_span_err!(span, "suggestion without `code = \"...\"`")
}
}
}
_ => {
throw_invalid_attr!(attr)
}
};
let mut code = None;
let mut suggestion_kind = None;
let mut first = true;
let mut slug = None;
let mut no_span = false;
list.parse_nested_meta(|nested| {
if nested.input.is_empty() || nested.input.peek(Token![,]) {
if first {
slug = Some(nested.path);
} else if nested.path.is_ident("no_span") {
no_span = true;
} else {
span_err(nested.input.span().unwrap(), "a diagnostic slug must be the first argument to the attribute").emit();
}
first = false;
return Ok(());
}
first = false;
let nested_name = nested.path.segments.last().unwrap().ident.to_string();
let nested_name = nested_name.as_str();
let path_span = nested.path.span().unwrap();
let val_span = nested.input.span().unwrap();
macro_rules! get_string {
() => {{
let Ok(value) = nested.value().and_then(|x| x.parse::<LitStr>()) else {
span_err(val_span, "expected `= \"xxx\"`").emit();
return Ok(());
};
value
}};
}
let mut has_errors = false;
let input = nested.input;
match (nested_name, &mut kind) {
("code", SubdiagnosticKind::Suggestion { code_field, .. }) => {
let code_init = build_suggestion_code(
code_field,
nested,
fields,
AllowMultipleAlternatives::Yes,
);
code.set_once(code_init, path_span);
}
(
"applicability",
SubdiagnosticKind::Suggestion { ref mut applicability, .. }
| SubdiagnosticKind::MultipartSuggestion { ref mut applicability, .. },
) => {
let value = get_string!();
let value = Applicability::from_str(&value.value()).unwrap_or_else(|()| {
span_err(value.span().unwrap(), "invalid applicability").emit();
has_errors = true;
Applicability::Unspecified
});
applicability.set_once(value, span);
}
(
"style",
SubdiagnosticKind::Suggestion { .. }
| SubdiagnosticKind::MultipartSuggestion { .. },
) => {
let value = get_string!();
let value = value.value().parse().unwrap_or_else(|()| {
span_err(value.span().unwrap(), "invalid suggestion style")
.help("valid styles are `normal`, `short`, `hidden`, `verbose` and `tool-only`")
.emit();
has_errors = true;
SuggestionKind::Normal
});
suggestion_kind.set_once(value, span);
}
(_, SubdiagnosticKind::Suggestion { .. }) => {
span_err(path_span, "invalid nested attribute")
.help(
"only `no_span`, `style`, `code` and `applicability` are valid nested attributes",
)
.emit();
has_errors = true;
}
(_, SubdiagnosticKind::MultipartSuggestion { .. }) => {
span_err(path_span, "invalid nested attribute")
.help("only `no_span`, `style` and `applicability` are valid nested attributes")
.emit();
has_errors = true;
}
_ => {
span_err(path_span, "only `no_span` is a valid nested attribute").emit();
has_errors = true;
}
}
if has_errors {
let _ = input.parse::<TokenStream>();
}
Ok(())
})?;
match kind {
SubdiagnosticKind::Suggestion {
ref code_field,
ref mut code_init,
suggestion_kind: ref mut kind_field,
..
} => {
if let Some(kind) = suggestion_kind.value() {
*kind_field = kind;
}
*code_init = if let Some(init) = code.value() {
init
} else {
span_err(span, "suggestion without `code = \"...\"`").emit();
quote! { let #code_field = std::iter::empty(); }
};
}
SubdiagnosticKind::MultipartSuggestion {
suggestion_kind: ref mut kind_field, ..
} => {
if let Some(kind) = suggestion_kind.value() {
*kind_field = kind;
}
}
SubdiagnosticKind::Label
| SubdiagnosticKind::Note
| SubdiagnosticKind::NoteOnce
| SubdiagnosticKind::Help
| SubdiagnosticKind::HelpOnce
| SubdiagnosticKind::Warn => {}
}
Ok(Some(SubdiagnosticVariant { kind, slug, no_span }))
}
}
impl quote::IdentFragment for SubdiagnosticKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SubdiagnosticKind::Label => write!(f, "label"),
SubdiagnosticKind::Note => write!(f, "note"),
SubdiagnosticKind::NoteOnce => write!(f, "note_once"),
SubdiagnosticKind::Help => write!(f, "help"),
SubdiagnosticKind::HelpOnce => write!(f, "help_once"),
SubdiagnosticKind::Warn => write!(f, "warn"),
SubdiagnosticKind::Suggestion { .. } => write!(f, "suggestions_with_style"),
SubdiagnosticKind::MultipartSuggestion { .. } => {
write!(f, "multipart_suggestion_with_style")
}
}
}
fn span(&self) -> Option<proc_macro2::Span> {
None
}
}
pub(super) fn should_generate_arg(field: &Field) -> bool {
field.attrs.iter().all(|attr| is_doc_comment(attr))
}
pub(super) fn is_doc_comment(attr: &Attribute) -> bool {
attr.path().segments.last().unwrap().ident == "doc"
}