1use std::{
2 cell::RefCell,
3 collections::{BTreeSet, HashMap},
4 fmt,
5 str::FromStr,
6};
7
8use proc_macro::Span;
9use proc_macro2::{Ident, TokenStream};
10use quote::{ToTokens, format_ident, quote};
11use syn::{
12 Attribute, Field, LitStr, Meta, Path, Token, Type, TypeTuple, meta::ParseNestedMeta,
13 parenthesized, punctuated::Punctuated, spanned::Spanned,
14};
15use synstructure::{BindingInfo, VariantInfo};
16
17use super::error::invalid_attr;
18use crate::diagnostics::error::{
19 DiagnosticDeriveError, span_err, throw_invalid_attr, throw_span_err,
20};
21
22thread_local! {
23 pub(crate) static CODE_IDENT_COUNT: RefCell<u32> = RefCell::new(0);
24}
25
26pub(crate) fn new_code_ident() -> syn::Ident {
28 CODE_IDENT_COUNT.with(|count| {
29 let ident = format_ident!("__code_{}", *count.borrow());
30 *count.borrow_mut() += 1;
31 ident
32 })
33}
34
35pub(crate) fn type_matches_path(ty: &Type, name: &[&str]) -> bool {
40 if let Type::Path(ty) = ty {
41 ty.path
42 .segments
43 .iter()
44 .map(|s| s.ident.to_string())
45 .rev()
46 .zip(name.iter().rev())
47 .all(|(x, y)| &x.as_str() == y)
48 } else {
49 false
50 }
51}
52
53pub(crate) fn type_is_unit(ty: &Type) -> bool {
55 if let Type::Tuple(TypeTuple { elems, .. }) = ty { elems.is_empty() } else { false }
56}
57
58pub(crate) fn type_is_bool(ty: &Type) -> bool {
60 type_matches_path(ty, &["bool"])
61}
62
63pub(crate) fn report_type_error(
65 attr: &Attribute,
66 ty_name: &str,
67) -> Result<!, DiagnosticDeriveError> {
68 let name = attr.path().segments.last().unwrap().ident.to_string();
69 let meta = &attr.meta;
70
71 throw_span_err!(
72 attr.span().unwrap(),
73 &format!(
74 "the `#[{}{}]` attribute can only be applied to fields of type {}",
75 name,
76 match meta {
77 Meta::Path(_) => "",
78 Meta::NameValue(_) => " = ...",
79 Meta::List(_) => "(...)",
80 },
81 ty_name
82 )
83 );
84}
85
86fn report_error_if_not_applied_to_ty(
88 attr: &Attribute,
89 info: &FieldInfo<'_>,
90 path: &[&str],
91 ty_name: &str,
92) -> Result<(), DiagnosticDeriveError> {
93 if !type_matches_path(info.ty.inner_type(), path) {
94 report_type_error(attr, ty_name)?;
95 }
96
97 Ok(())
98}
99
100pub(crate) fn report_error_if_not_applied_to_applicability(
102 attr: &Attribute,
103 info: &FieldInfo<'_>,
104) -> Result<(), DiagnosticDeriveError> {
105 report_error_if_not_applied_to_ty(
106 attr,
107 info,
108 &["rustc_errors", "Applicability"],
109 "`Applicability`",
110 )
111}
112
113pub(crate) fn report_error_if_not_applied_to_span(
115 attr: &Attribute,
116 info: &FieldInfo<'_>,
117) -> Result<(), DiagnosticDeriveError> {
118 if !type_matches_path(info.ty.inner_type(), &["rustc_span", "Span"])
119 && !type_matches_path(info.ty.inner_type(), &["rustc_errors", "MultiSpan"])
120 {
121 report_type_error(attr, "`Span` or `MultiSpan`")?;
122 }
123
124 Ok(())
125}
126
127#[derive(Copy, Clone)]
129pub(crate) enum FieldInnerTy<'ty> {
130 Option(&'ty Type),
132 Vec(&'ty Type),
134 Plain(&'ty Type),
136}
137
138impl<'ty> FieldInnerTy<'ty> {
139 pub(crate) fn from_type(ty: &'ty Type) -> Self {
145 fn single_generic_type(ty: &Type) -> &Type {
146 let Type::Path(ty_path) = ty else {
147 panic!("expected path type");
148 };
149
150 let path = &ty_path.path;
151 let ty = path.segments.iter().last().unwrap();
152 let syn::PathArguments::AngleBracketed(bracketed) = &ty.arguments else {
153 panic!("expected bracketed generic arguments")
154 };
155
156 assert_eq!(bracketed.args.len(), 1);
157
158 let syn::GenericArgument::Type(ty) = &bracketed.args[0] else {
159 panic!("expected generic parameter to be a type generic");
160 };
161
162 ty
163 }
164
165 if type_matches_path(ty, &["std", "option", "Option"]) {
166 FieldInnerTy::Option(single_generic_type(ty))
167 } else if type_matches_path(ty, &["std", "vec", "Vec"]) {
168 FieldInnerTy::Vec(single_generic_type(ty))
169 } else {
170 FieldInnerTy::Plain(ty)
171 }
172 }
173
174 pub(crate) fn will_iterate(&self) -> bool {
177 match self {
178 FieldInnerTy::Vec(..) => true,
179 FieldInnerTy::Option(..) | FieldInnerTy::Plain(_) => false,
180 }
181 }
182
183 pub(crate) fn inner_type(&self) -> &'ty Type {
185 match self {
186 FieldInnerTy::Option(inner) | FieldInnerTy::Vec(inner) | FieldInnerTy::Plain(inner) => {
187 inner
188 }
189 }
190 }
191
192 pub(crate) fn with(&self, binding: impl ToTokens, inner: impl ToTokens) -> TokenStream {
194 match self {
195 FieldInnerTy::Option(..) => {
196 quote! {
197 if let Some(#binding) = #binding {
198 #inner
199 }
200 }
201 }
202 FieldInnerTy::Vec(..) => {
203 quote! {
204 for #binding in #binding {
205 #inner
206 }
207 }
208 }
209 FieldInnerTy::Plain(t) if type_is_bool(t) => {
210 quote! {
211 if #binding {
212 #inner
213 }
214 }
215 }
216 FieldInnerTy::Plain(..) => quote! { #inner },
217 }
218 }
219
220 pub(crate) fn span(&self) -> proc_macro2::Span {
221 match self {
222 FieldInnerTy::Option(ty) | FieldInnerTy::Vec(ty) | FieldInnerTy::Plain(ty) => ty.span(),
223 }
224 }
225}
226
227pub(crate) struct FieldInfo<'a> {
230 pub(crate) binding: &'a BindingInfo<'a>,
231 pub(crate) ty: FieldInnerTy<'a>,
232 pub(crate) span: &'a proc_macro2::Span,
233}
234
235pub(crate) trait SetOnce<T> {
238 fn set_once(&mut self, value: T, span: Span);
239
240 fn value(self) -> Option<T>;
241 fn value_ref(&self) -> Option<&T>;
242}
243
244pub(super) type SpannedOption<T> = Option<(T, Span)>;
246
247impl<T> SetOnce<T> for SpannedOption<T> {
248 fn set_once(&mut self, value: T, span: Span) {
249 match self {
250 None => {
251 *self = Some((value, span));
252 }
253 Some((_, prev_span)) => {
254 span_err(span, "specified multiple times")
255 .span_note(*prev_span, "previously specified here")
256 .emit();
257 }
258 }
259 }
260
261 fn value(self) -> Option<T> {
262 self.map(|(v, _)| v)
263 }
264
265 fn value_ref(&self) -> Option<&T> {
266 self.as_ref().map(|(v, _)| v)
267 }
268}
269
270pub(super) type FieldMap = HashMap<String, TokenStream>;
271
272pub(crate) trait HasFieldMap {
273 fn get_field_binding(&self, field: &String) -> Option<&TokenStream>;
275
276 fn build_format(&self, input: &str, span: proc_macro2::Span) -> TokenStream {
299 let mut referenced_fields: BTreeSet<String> = BTreeSet::new();
303
304 let mut it = input.chars().peekable();
306
307 while let Some(c) = it.next() {
311 if c != '{' {
312 continue;
313 }
314 if *it.peek().unwrap_or(&'\0') == '{' {
315 assert_eq!(it.next().unwrap(), '{');
316 continue;
317 }
318 let mut eat_argument = || -> Option<String> {
319 let mut result = String::new();
320 while let Some(c) = it.next() {
326 result.push(c);
327 let next = *it.peek().unwrap_or(&'\0');
328 if next == '}' {
329 break;
330 } else if next == ':' {
331 assert_eq!(it.next().unwrap(), ':');
333 break;
334 }
335 }
336 while it.next()? != '}' {
338 continue;
339 }
340 Some(result)
341 };
342
343 if let Some(referenced_field) = eat_argument() {
344 referenced_fields.insert(referenced_field);
345 }
346 }
347
348 let args = referenced_fields.into_iter().map(|field: String| {
352 let field_ident = format_ident!("{}", field);
353 let value = match self.get_field_binding(&field) {
354 Some(value) => value.clone(),
355 None => {
357 span_err(
358 span.unwrap(),
359 format!("`{field}` doesn't refer to a field on this type"),
360 )
361 .emit();
362 quote! {
363 "{#field}"
364 }
365 }
366 };
367 quote! {
368 #field_ident = #value
369 }
370 });
371 quote! {
372 format!(#input #(,#args)*)
373 }
374 }
375}
376
377#[derive(Clone, Copy)]
380pub(crate) enum Applicability {
381 MachineApplicable,
382 MaybeIncorrect,
383 HasPlaceholders,
384 Unspecified,
385}
386
387impl FromStr for Applicability {
388 type Err = ();
389
390 fn from_str(s: &str) -> Result<Self, Self::Err> {
391 match s {
392 "machine-applicable" => Ok(Applicability::MachineApplicable),
393 "maybe-incorrect" => Ok(Applicability::MaybeIncorrect),
394 "has-placeholders" => Ok(Applicability::HasPlaceholders),
395 "unspecified" => Ok(Applicability::Unspecified),
396 _ => Err(()),
397 }
398 }
399}
400
401impl quote::ToTokens for Applicability {
402 fn to_tokens(&self, tokens: &mut TokenStream) {
403 tokens.extend(match self {
404 Applicability::MachineApplicable => {
405 quote! { rustc_errors::Applicability::MachineApplicable }
406 }
407 Applicability::MaybeIncorrect => {
408 quote! { rustc_errors::Applicability::MaybeIncorrect }
409 }
410 Applicability::HasPlaceholders => {
411 quote! { rustc_errors::Applicability::HasPlaceholders }
412 }
413 Applicability::Unspecified => {
414 quote! { rustc_errors::Applicability::Unspecified }
415 }
416 });
417 }
418}
419
420pub(super) fn build_field_mapping(variant: &VariantInfo<'_>) -> HashMap<String, TokenStream> {
423 let mut fields_map = FieldMap::new();
424 for binding in variant.bindings() {
425 if let Some(ident) = &binding.ast().ident {
426 fields_map.insert(ident.to_string(), quote! { #binding });
427 }
428 }
429 fields_map
430}
431
432#[derive(Copy, Clone, Debug)]
433pub(super) enum AllowMultipleAlternatives {
434 No,
435 Yes,
436}
437
438fn parse_suggestion_values(
439 nested: ParseNestedMeta<'_>,
440 allow_multiple: AllowMultipleAlternatives,
441) -> syn::Result<Vec<LitStr>> {
442 let values = if let Ok(val) = nested.value() {
443 vec![val.parse()?]
444 } else {
445 let content;
446 parenthesized!(content in nested.input);
447
448 if let AllowMultipleAlternatives::No = allow_multiple {
449 span_err(
450 nested.input.span().unwrap(),
451 "expected exactly one string literal for `code = ...`",
452 )
453 .emit();
454 vec![]
455 } else {
456 let literals = Punctuated::<LitStr, Token![,]>::parse_terminated(&content);
457
458 match literals {
459 Ok(p) if p.is_empty() => {
460 span_err(
461 content.span().unwrap(),
462 "expected at least one string literal for `code(...)`",
463 )
464 .emit();
465 vec![]
466 }
467 Ok(p) => p.into_iter().collect(),
468 Err(_) => {
469 span_err(
470 content.span().unwrap(),
471 "`code(...)` must contain only string literals",
472 )
473 .emit();
474 vec![]
475 }
476 }
477 }
478 };
479
480 Ok(values)
481}
482
483pub(super) fn build_suggestion_code(
486 code_field: &Ident,
487 nested: ParseNestedMeta<'_>,
488 fields: &impl HasFieldMap,
489 allow_multiple: AllowMultipleAlternatives,
490) -> TokenStream {
491 let values = match parse_suggestion_values(nested, allow_multiple) {
492 Ok(x) => x,
493 Err(e) => return e.into_compile_error(),
494 };
495
496 if let AllowMultipleAlternatives::Yes = allow_multiple {
497 let formatted_strings: Vec<_> = values
498 .into_iter()
499 .map(|value| fields.build_format(&value.value(), value.span()))
500 .collect();
501 quote! { let #code_field = [#(#formatted_strings),*].into_iter(); }
502 } else if let [value] = values.as_slice() {
503 let formatted_str = fields.build_format(&value.value(), value.span());
504 quote! { let #code_field = #formatted_str; }
505 } else {
506 quote! { let #code_field = String::new(); }
508 }
509}
510
511#[derive(Clone, Copy, PartialEq)]
513pub(super) enum SuggestionKind {
514 Normal,
515 Short,
516 Hidden,
517 Verbose,
518 ToolOnly,
519}
520
521impl FromStr for SuggestionKind {
522 type Err = ();
523
524 fn from_str(s: &str) -> Result<Self, Self::Err> {
525 match s {
526 "normal" => Ok(SuggestionKind::Normal),
527 "short" => Ok(SuggestionKind::Short),
528 "hidden" => Ok(SuggestionKind::Hidden),
529 "verbose" => Ok(SuggestionKind::Verbose),
530 "tool-only" => Ok(SuggestionKind::ToolOnly),
531 _ => Err(()),
532 }
533 }
534}
535
536impl fmt::Display for SuggestionKind {
537 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
538 match self {
539 SuggestionKind::Normal => write!(f, "normal"),
540 SuggestionKind::Short => write!(f, "short"),
541 SuggestionKind::Hidden => write!(f, "hidden"),
542 SuggestionKind::Verbose => write!(f, "verbose"),
543 SuggestionKind::ToolOnly => write!(f, "tool-only"),
544 }
545 }
546}
547
548impl SuggestionKind {
549 pub(crate) fn to_suggestion_style(&self) -> TokenStream {
550 match self {
551 SuggestionKind::Normal => {
552 quote! { rustc_errors::SuggestionStyle::ShowCode }
553 }
554 SuggestionKind::Short => {
555 quote! { rustc_errors::SuggestionStyle::HideCodeInline }
556 }
557 SuggestionKind::Hidden => {
558 quote! { rustc_errors::SuggestionStyle::HideCodeAlways }
559 }
560 SuggestionKind::Verbose => {
561 quote! { rustc_errors::SuggestionStyle::ShowAlways }
562 }
563 SuggestionKind::ToolOnly => {
564 quote! { rustc_errors::SuggestionStyle::CompletelyHidden }
565 }
566 }
567 }
568
569 fn from_suffix(s: &str) -> Option<Self> {
570 match s {
571 "" => Some(SuggestionKind::Normal),
572 "_short" => Some(SuggestionKind::Short),
573 "_hidden" => Some(SuggestionKind::Hidden),
574 "_verbose" => Some(SuggestionKind::Verbose),
575 _ => None,
576 }
577 }
578}
579
580#[derive(Clone)]
582pub(super) enum SubdiagnosticKind {
583 Label,
585 Note,
587 NoteOnce,
589 Help,
591 HelpOnce,
593 Warn,
595 Suggestion {
597 suggestion_kind: SuggestionKind,
598 applicability: SpannedOption<Applicability>,
599 code_field: syn::Ident,
602 code_init: TokenStream,
605 },
606 MultipartSuggestion {
608 suggestion_kind: SuggestionKind,
609 applicability: SpannedOption<Applicability>,
610 },
611}
612
613pub(super) struct SubdiagnosticVariant {
614 pub(super) kind: SubdiagnosticKind,
615 pub(super) slug: Option<Path>,
616 pub(super) no_span: bool,
617}
618
619impl SubdiagnosticVariant {
620 pub(super) fn from_attr(
624 attr: &Attribute,
625 fields: &impl HasFieldMap,
626 ) -> Result<Option<SubdiagnosticVariant>, DiagnosticDeriveError> {
627 if is_doc_comment(attr) {
629 return Ok(None);
630 }
631
632 let span = attr.span().unwrap();
633
634 let name = attr.path().segments.last().unwrap().ident.to_string();
635 let name = name.as_str();
636
637 let mut kind = match name {
638 "label" => SubdiagnosticKind::Label,
639 "note" => SubdiagnosticKind::Note,
640 "note_once" => SubdiagnosticKind::NoteOnce,
641 "help" => SubdiagnosticKind::Help,
642 "help_once" => SubdiagnosticKind::HelpOnce,
643 "warning" => SubdiagnosticKind::Warn,
644 _ => {
645 if let Some(suggestion_kind) = name
648 .strip_prefix("suggestion")
649 .and_then(SuggestionKind::from_suffix)
650 {
651 if suggestion_kind != SuggestionKind::Normal {
652 invalid_attr(attr)
653 .help(format!(
654 r#"Use `#[suggestion(..., style = "{suggestion_kind}")]` instead"#
655 ))
656 .emit();
657 }
658
659 SubdiagnosticKind::Suggestion {
660 suggestion_kind: SuggestionKind::Normal,
661 applicability: None,
662 code_field: new_code_ident(),
663 code_init: TokenStream::new(),
664 }
665 } else if let Some(suggestion_kind) = name
666 .strip_prefix("multipart_suggestion")
667 .and_then(SuggestionKind::from_suffix)
668 {
669 if suggestion_kind != SuggestionKind::Normal {
670 invalid_attr(attr)
671 .help(format!(
672 r#"Use `#[multipart_suggestion(..., style = "{suggestion_kind}")]` instead"#
673 ))
674 .emit();
675 }
676
677 SubdiagnosticKind::MultipartSuggestion {
678 suggestion_kind: SuggestionKind::Normal,
679 applicability: None,
680 }
681 } else {
682 throw_invalid_attr!(attr);
683 }
684 }
685 };
686
687 let list = match &attr.meta {
688 Meta::List(list) => {
689 list
692 }
693 Meta::Path(_) => {
694 match kind {
700 SubdiagnosticKind::Label
701 | SubdiagnosticKind::Note
702 | SubdiagnosticKind::NoteOnce
703 | SubdiagnosticKind::Help
704 | SubdiagnosticKind::HelpOnce
705 | SubdiagnosticKind::Warn
706 | SubdiagnosticKind::MultipartSuggestion { .. } => {
707 return Ok(Some(SubdiagnosticVariant { kind, slug: None, no_span: false }));
708 }
709 SubdiagnosticKind::Suggestion { .. } => {
710 throw_span_err!(span, "suggestion without `code = \"...\"`")
711 }
712 }
713 }
714 _ => {
715 throw_invalid_attr!(attr)
716 }
717 };
718
719 let mut code = None;
720 let mut suggestion_kind = None;
721
722 let mut first = true;
723 let mut slug = None;
724 let mut no_span = false;
725
726 list.parse_nested_meta(|nested| {
727 if nested.input.is_empty() || nested.input.peek(Token![,]) {
728 if first {
729 slug = Some(nested.path);
730 } else if nested.path.is_ident("no_span") {
731 no_span = true;
732 } else {
733 span_err(nested.input.span().unwrap(), "a diagnostic slug must be the first argument to the attribute").emit();
734 }
735
736 first = false;
737 return Ok(());
738 }
739
740 first = false;
741
742 let nested_name = nested.path.segments.last().unwrap().ident.to_string();
743 let nested_name = nested_name.as_str();
744
745 let path_span = nested.path.span().unwrap();
746 let val_span = nested.input.span().unwrap();
747
748 macro_rules! get_string {
749 () => {{
750 let Ok(value) = nested.value().and_then(|x| x.parse::<LitStr>()) else {
751 span_err(val_span, "expected `= \"xxx\"`").emit();
752 return Ok(());
753 };
754 value
755 }};
756 }
757
758 let mut has_errors = false;
759 let input = nested.input;
760
761 match (nested_name, &mut kind) {
762 ("code", SubdiagnosticKind::Suggestion { code_field, .. }) => {
763 let code_init = build_suggestion_code(
764 code_field,
765 nested,
766 fields,
767 AllowMultipleAlternatives::Yes,
768 );
769 code.set_once(code_init, path_span);
770 }
771 (
772 "applicability",
773 SubdiagnosticKind::Suggestion { applicability, .. }
774 | SubdiagnosticKind::MultipartSuggestion { applicability, .. },
775 ) => {
776 let value = get_string!();
777 let value = Applicability::from_str(&value.value()).unwrap_or_else(|()| {
778 span_err(value.span().unwrap(), "invalid applicability").emit();
779 has_errors = true;
780 Applicability::Unspecified
781 });
782 applicability.set_once(value, span);
783 }
784 (
785 "style",
786 SubdiagnosticKind::Suggestion { .. }
787 | SubdiagnosticKind::MultipartSuggestion { .. },
788 ) => {
789 let value = get_string!();
790
791 let value = value.value().parse().unwrap_or_else(|()| {
792 span_err(value.span().unwrap(), "invalid suggestion style")
793 .help("valid styles are `normal`, `short`, `hidden`, `verbose` and `tool-only`")
794 .emit();
795 has_errors = true;
796 SuggestionKind::Normal
797 });
798
799 suggestion_kind.set_once(value, span);
800 }
801
802 (_, SubdiagnosticKind::Suggestion { .. }) => {
804 span_err(path_span, "invalid nested attribute")
805 .help(
806 "only `no_span`, `style`, `code` and `applicability` are valid nested attributes",
807 )
808 .emit();
809 has_errors = true;
810 }
811 (_, SubdiagnosticKind::MultipartSuggestion { .. }) => {
812 span_err(path_span, "invalid nested attribute")
813 .help("only `no_span`, `style` and `applicability` are valid nested attributes")
814 .emit();
815 has_errors = true;
816 }
817 _ => {
818 span_err(path_span, "only `no_span` is a valid nested attribute").emit();
819 has_errors = true;
820 }
821 }
822
823 if has_errors {
824 let _ = input.parse::<TokenStream>();
826 }
827
828 Ok(())
829 })?;
830
831 match kind {
832 SubdiagnosticKind::Suggestion {
833 ref code_field,
834 ref mut code_init,
835 suggestion_kind: ref mut kind_field,
836 ..
837 } => {
838 if let Some(kind) = suggestion_kind.value() {
839 *kind_field = kind;
840 }
841
842 *code_init = if let Some(init) = code.value() {
843 init
844 } else {
845 span_err(span, "suggestion without `code = \"...\"`").emit();
846 quote! { let #code_field = std::iter::empty(); }
847 };
848 }
849 SubdiagnosticKind::MultipartSuggestion {
850 suggestion_kind: ref mut kind_field, ..
851 } => {
852 if let Some(kind) = suggestion_kind.value() {
853 *kind_field = kind;
854 }
855 }
856 SubdiagnosticKind::Label
857 | SubdiagnosticKind::Note
858 | SubdiagnosticKind::NoteOnce
859 | SubdiagnosticKind::Help
860 | SubdiagnosticKind::HelpOnce
861 | SubdiagnosticKind::Warn => {}
862 }
863
864 Ok(Some(SubdiagnosticVariant { kind, slug, no_span }))
865 }
866}
867
868impl quote::IdentFragment for SubdiagnosticKind {
869 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
870 match self {
871 SubdiagnosticKind::Label => write!(f, "label"),
872 SubdiagnosticKind::Note => write!(f, "note"),
873 SubdiagnosticKind::NoteOnce => write!(f, "note_once"),
874 SubdiagnosticKind::Help => write!(f, "help"),
875 SubdiagnosticKind::HelpOnce => write!(f, "help_once"),
876 SubdiagnosticKind::Warn => write!(f, "warn"),
877 SubdiagnosticKind::Suggestion { .. } => write!(f, "suggestions_with_style"),
878 SubdiagnosticKind::MultipartSuggestion { .. } => {
879 write!(f, "multipart_suggestion_with_style")
880 }
881 }
882 }
883
884 fn span(&self) -> Option<proc_macro2::Span> {
885 None
886 }
887}
888
889pub(super) fn should_generate_arg(field: &Field) -> bool {
892 field.attrs.iter().all(|attr| is_doc_comment(attr))
894}
895
896pub(super) fn is_doc_comment(attr: &Attribute) -> bool {
897 attr.path().segments.last().unwrap().ident == "doc"
898}