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