rustc_attr_parsing/attributes/
doc.rs

1use rustc_ast::ast::{AttrStyle, LitKind, MetaItemLit};
2use rustc_feature::template;
3use rustc_hir::attrs::{
4    AttributeKind, CfgEntry, CfgHideShow, CfgInfo, DocAttribute, DocInline, HideOrShow,
5};
6use rustc_hir::lints::AttributeLintKind;
7use rustc_span::{Span, Symbol, edition, sym};
8use thin_vec::ThinVec;
9
10use super::prelude::{ALL_TARGETS, AllowedTargets};
11use super::{AcceptMapping, AttributeParser};
12use crate::context::{AcceptContext, FinalizeContext, Stage};
13use crate::parser::{ArgParser, MetaItemOrLitParser, MetaItemParser, OwnedPathParser};
14use crate::session_diagnostics::{
15    DocAliasBadChar, DocAliasEmpty, DocAliasMalformed, DocAliasStartEnd, DocAttributeNotAttribute,
16    DocKeywordNotKeyword,
17};
18
19fn check_keyword<S: Stage>(cx: &mut AcceptContext<'_, '_, S>, keyword: Symbol, span: Span) -> bool {
20    // FIXME: Once rustdoc can handle URL conflicts on case insensitive file systems, we
21    // can remove the `SelfTy` case here, remove `sym::SelfTy`, and update the
22    // `#[doc(keyword = "SelfTy")` attribute in `library/std/src/keyword_docs.rs`.
23    if keyword.is_reserved(|| edition::LATEST_STABLE_EDITION)
24        || keyword.is_weak()
25        || keyword == sym::SelfTy
26    {
27        return true;
28    }
29    cx.emit_err(DocKeywordNotKeyword { span, keyword });
30    false
31}
32
33fn check_attribute<S: Stage>(
34    cx: &mut AcceptContext<'_, '_, S>,
35    attribute: Symbol,
36    span: Span,
37) -> bool {
38    // FIXME: This should support attributes with namespace like `diagnostic::do_not_recommend`.
39    if rustc_feature::BUILTIN_ATTRIBUTE_MAP.contains_key(&attribute) {
40        return true;
41    }
42    cx.emit_err(DocAttributeNotAttribute { span, attribute });
43    false
44}
45
46fn parse_keyword_and_attribute<S, F>(
47    cx: &mut AcceptContext<'_, '_, S>,
48    path: &OwnedPathParser,
49    args: &ArgParser,
50    attr_value: &mut Option<(Symbol, Span)>,
51    callback: F,
52) where
53    S: Stage,
54    F: FnOnce(&mut AcceptContext<'_, '_, S>, Symbol, Span) -> bool,
55{
56    let Some(nv) = args.name_value() else {
57        cx.expected_name_value(args.span().unwrap_or(path.span()), path.word_sym());
58        return;
59    };
60
61    let Some(value) = nv.value_as_str() else {
62        cx.expected_string_literal(nv.value_span, Some(nv.value_as_lit()));
63        return;
64    };
65
66    if !callback(cx, value, nv.value_span) {
67        return;
68    }
69
70    if attr_value.is_some() {
71        cx.duplicate_key(path.span(), path.word_sym().unwrap());
72        return;
73    }
74
75    *attr_value = Some((value, path.span()));
76}
77
78#[derive(Default, Debug)]
79pub(crate) struct DocParser {
80    attribute: DocAttribute,
81    nb_doc_attrs: usize,
82}
83
84impl DocParser {
85    fn parse_single_test_doc_attr_item<S: Stage>(
86        &mut self,
87        cx: &mut AcceptContext<'_, '_, S>,
88        mip: &MetaItemParser,
89    ) {
90        let path = mip.path();
91        let args = mip.args();
92
93        match path.word_sym() {
94            Some(sym::no_crate_inject) => {
95                if let Err(span) = args.no_args() {
96                    cx.expected_no_args(span);
97                    return;
98                }
99
100                if self.attribute.no_crate_inject.is_some() {
101                    cx.duplicate_key(path.span(), sym::no_crate_inject);
102                    return;
103                }
104
105                self.attribute.no_crate_inject = Some(path.span())
106            }
107            Some(sym::attr) => {
108                let Some(list) = args.list() else {
109                    cx.expected_list(cx.attr_span, args);
110                    return;
111                };
112
113                // FIXME: convert list into a Vec of `AttributeKind` because current code is awful.
114                for attr in list.mixed() {
115                    self.attribute.test_attrs.push(attr.span());
116                }
117            }
118            Some(name) => {
119                cx.emit_lint(
120                    rustc_session::lint::builtin::INVALID_DOC_ATTRIBUTES,
121                    AttributeLintKind::DocTestUnknown { name },
122                    path.span(),
123                );
124            }
125            None => {
126                cx.emit_lint(
127                    rustc_session::lint::builtin::INVALID_DOC_ATTRIBUTES,
128                    AttributeLintKind::DocTestLiteral,
129                    path.span(),
130                );
131            }
132        }
133    }
134
135    fn add_alias<S: Stage>(
136        &mut self,
137        cx: &mut AcceptContext<'_, '_, S>,
138        alias: Symbol,
139        span: Span,
140    ) {
141        let attr_str = "`#[doc(alias = \"...\")]`";
142        if alias == sym::empty {
143            cx.emit_err(DocAliasEmpty { span, attr_str });
144            return;
145        }
146
147        let alias_str = alias.as_str();
148        if let Some(c) =
149            alias_str.chars().find(|&c| c == '"' || c == '\'' || (c.is_whitespace() && c != ' '))
150        {
151            cx.emit_err(DocAliasBadChar { span, attr_str, char_: c });
152            return;
153        }
154        if alias_str.starts_with(' ') || alias_str.ends_with(' ') {
155            cx.emit_err(DocAliasStartEnd { span, attr_str });
156            return;
157        }
158
159        if let Some(first_definition) = self.attribute.aliases.get(&alias).copied() {
160            cx.emit_lint(
161                rustc_session::lint::builtin::UNUSED_ATTRIBUTES,
162                AttributeLintKind::DuplicateDocAlias { first_definition },
163                span,
164            );
165        }
166
167        self.attribute.aliases.insert(alias, span);
168    }
169
170    fn parse_alias<S: Stage>(
171        &mut self,
172        cx: &mut AcceptContext<'_, '_, S>,
173        path: &OwnedPathParser,
174        args: &ArgParser,
175    ) {
176        match args {
177            ArgParser::NoArgs => {
178                cx.emit_err(DocAliasMalformed { span: args.span().unwrap_or(path.span()) });
179            }
180            ArgParser::List(list) => {
181                for i in list.mixed() {
182                    let Some(alias) = i.lit().and_then(|i| i.value_str()) else {
183                        cx.expected_string_literal(i.span(), i.lit());
184                        continue;
185                    };
186
187                    self.add_alias(cx, alias, i.span());
188                }
189            }
190            ArgParser::NameValue(nv) => {
191                let Some(alias) = nv.value_as_str() else {
192                    cx.expected_string_literal(nv.value_span, Some(nv.value_as_lit()));
193                    return;
194                };
195                self.add_alias(cx, alias, nv.value_span);
196            }
197        }
198    }
199
200    fn parse_inline<S: Stage>(
201        &mut self,
202        cx: &mut AcceptContext<'_, '_, S>,
203        path: &OwnedPathParser,
204        args: &ArgParser,
205        inline: DocInline,
206    ) {
207        if let Err(span) = args.no_args() {
208            cx.expected_no_args(span);
209            return;
210        }
211
212        self.attribute.inline.push((inline, path.span()));
213    }
214
215    fn parse_cfg<S: Stage>(&mut self, cx: &mut AcceptContext<'_, '_, S>, args: &ArgParser) {
216        // This function replaces cases like `cfg(all())` with `true`.
217        fn simplify_cfg(cfg_entry: &mut CfgEntry) {
218            match cfg_entry {
219                CfgEntry::All(cfgs, span) if cfgs.is_empty() => {
220                    *cfg_entry = CfgEntry::Bool(true, *span)
221                }
222                CfgEntry::Any(cfgs, span) if cfgs.is_empty() => {
223                    *cfg_entry = CfgEntry::Bool(false, *span)
224                }
225                CfgEntry::Not(cfg, _) => simplify_cfg(cfg),
226                _ => {}
227            }
228        }
229        if let Some(mut cfg_entry) = super::cfg::parse_cfg(cx, args) {
230            simplify_cfg(&mut cfg_entry);
231            self.attribute.cfg.push(cfg_entry);
232        }
233    }
234
235    fn parse_auto_cfg<S: Stage>(
236        &mut self,
237        cx: &mut AcceptContext<'_, '_, S>,
238        path: &OwnedPathParser,
239        args: &ArgParser,
240    ) {
241        match args {
242            ArgParser::NoArgs => {
243                self.attribute.auto_cfg_change.push((true, path.span()));
244            }
245            ArgParser::List(list) => {
246                for meta in list.mixed() {
247                    let MetaItemOrLitParser::MetaItemParser(item) = meta else {
248                        cx.emit_lint(
249                            rustc_session::lint::builtin::INVALID_DOC_ATTRIBUTES,
250                            AttributeLintKind::DocAutoCfgExpectsHideOrShow,
251                            meta.span(),
252                        );
253                        continue;
254                    };
255                    let (kind, attr_name) = match item.path().word_sym() {
256                        Some(sym::hide) => (HideOrShow::Hide, sym::hide),
257                        Some(sym::show) => (HideOrShow::Show, sym::show),
258                        _ => {
259                            cx.emit_lint(
260                                rustc_session::lint::builtin::INVALID_DOC_ATTRIBUTES,
261                                AttributeLintKind::DocAutoCfgExpectsHideOrShow,
262                                item.span(),
263                            );
264                            continue;
265                        }
266                    };
267                    let ArgParser::List(list) = item.args() else {
268                        cx.emit_lint(
269                            rustc_session::lint::builtin::INVALID_DOC_ATTRIBUTES,
270                            AttributeLintKind::DocAutoCfgHideShowExpectsList { attr_name },
271                            item.span(),
272                        );
273                        continue;
274                    };
275
276                    let mut cfg_hide_show = CfgHideShow { kind, values: ThinVec::new() };
277
278                    for item in list.mixed() {
279                        let MetaItemOrLitParser::MetaItemParser(sub_item) = item else {
280                            cx.emit_lint(
281                                rustc_session::lint::builtin::INVALID_DOC_ATTRIBUTES,
282                                AttributeLintKind::DocAutoCfgHideShowUnexpectedItem { attr_name },
283                                item.span(),
284                            );
285                            continue;
286                        };
287                        match sub_item.args() {
288                            a @ (ArgParser::NoArgs | ArgParser::NameValue(_)) => {
289                                let Some(name) = sub_item.path().word_sym() else {
290                                    cx.expected_identifier(sub_item.path().span());
291                                    continue;
292                                };
293                                if let Ok(CfgEntry::NameValue { name, value, .. }) =
294                                    super::cfg::parse_name_value(
295                                        name,
296                                        sub_item.path().span(),
297                                        a.name_value(),
298                                        sub_item.span(),
299                                        cx,
300                                    )
301                                {
302                                    cfg_hide_show.values.push(CfgInfo {
303                                        name,
304                                        name_span: sub_item.path().span(),
305                                        // If `value` is `Some`, `a.name_value()` will always return
306                                        // `Some` as well.
307                                        value: value
308                                            .map(|v| (v, a.name_value().unwrap().value_span)),
309                                    })
310                                }
311                            }
312                            _ => {
313                                cx.emit_lint(
314                                    rustc_session::lint::builtin::INVALID_DOC_ATTRIBUTES,
315                                    AttributeLintKind::DocAutoCfgHideShowUnexpectedItem {
316                                        attr_name,
317                                    },
318                                    sub_item.span(),
319                                );
320                                continue;
321                            }
322                        }
323                    }
324                    self.attribute.auto_cfg.push((cfg_hide_show, path.span()));
325                }
326            }
327            ArgParser::NameValue(nv) => {
328                let MetaItemLit { kind: LitKind::Bool(bool_value), span, .. } = nv.value_as_lit()
329                else {
330                    cx.emit_lint(
331                        rustc_session::lint::builtin::INVALID_DOC_ATTRIBUTES,
332                        AttributeLintKind::DocAutoCfgWrongLiteral,
333                        nv.value_span,
334                    );
335                    return;
336                };
337                self.attribute.auto_cfg_change.push((*bool_value, *span));
338            }
339        }
340    }
341
342    fn parse_single_doc_attr_item<S: Stage>(
343        &mut self,
344        cx: &mut AcceptContext<'_, '_, S>,
345        mip: &MetaItemParser,
346    ) {
347        let path = mip.path();
348        let args = mip.args();
349
350        macro_rules! no_args {
351            ($ident: ident) => {{
352                if let Err(span) = args.no_args() {
353                    cx.expected_no_args(span);
354                    return;
355                }
356
357                // FIXME: It's errorring when the attribute is passed multiple times on the command
358                // line.
359                // The right fix for this would be to only check this rule if the attribute is
360                // not set on the command line but directly in the code.
361                // if self.attribute.$ident.is_some() {
362                //     cx.duplicate_key(path.span(), path.word_sym().unwrap());
363                //     return;
364                // }
365
366                self.attribute.$ident = Some(path.span());
367            }};
368        }
369        macro_rules! string_arg {
370            ($ident: ident) => {{
371                let Some(nv) = args.name_value() else {
372                    cx.expected_name_value(args.span().unwrap_or(path.span()), path.word_sym());
373                    return;
374                };
375
376                let Some(s) = nv.value_as_str() else {
377                    cx.expected_string_literal(nv.value_span, Some(nv.value_as_lit()));
378                    return;
379                };
380
381                // FIXME: It's errorring when the attribute is passed multiple times on the command
382                // line.
383                // The right fix for this would be to only check this rule if the attribute is
384                // not set on the command line but directly in the code.
385                // if self.attribute.$ident.is_some() {
386                //     cx.duplicate_key(path.span(), path.word_sym().unwrap());
387                //     return;
388                // }
389
390                self.attribute.$ident = Some((s, path.span()));
391            }};
392        }
393
394        match path.word_sym() {
395            Some(sym::alias) => self.parse_alias(cx, path, args),
396            Some(sym::hidden) => no_args!(hidden),
397            Some(sym::html_favicon_url) => string_arg!(html_favicon_url),
398            Some(sym::html_logo_url) => string_arg!(html_logo_url),
399            Some(sym::html_no_source) => no_args!(html_no_source),
400            Some(sym::html_playground_url) => string_arg!(html_playground_url),
401            Some(sym::html_root_url) => string_arg!(html_root_url),
402            Some(sym::issue_tracker_base_url) => string_arg!(issue_tracker_base_url),
403            Some(sym::inline) => self.parse_inline(cx, path, args, DocInline::Inline),
404            Some(sym::no_inline) => self.parse_inline(cx, path, args, DocInline::NoInline),
405            Some(sym::masked) => no_args!(masked),
406            Some(sym::cfg) => self.parse_cfg(cx, args),
407            Some(sym::notable_trait) => no_args!(notable_trait),
408            Some(sym::keyword) => parse_keyword_and_attribute(
409                cx,
410                path,
411                args,
412                &mut self.attribute.keyword,
413                check_keyword,
414            ),
415            Some(sym::attribute) => parse_keyword_and_attribute(
416                cx,
417                path,
418                args,
419                &mut self.attribute.attribute,
420                check_attribute,
421            ),
422            Some(sym::fake_variadic) => no_args!(fake_variadic),
423            Some(sym::search_unbox) => no_args!(search_unbox),
424            Some(sym::rust_logo) => no_args!(rust_logo),
425            Some(sym::auto_cfg) => self.parse_auto_cfg(cx, path, args),
426            Some(sym::test) => {
427                let Some(list) = args.list() else {
428                    cx.emit_lint(
429                        rustc_session::lint::builtin::INVALID_DOC_ATTRIBUTES,
430                        AttributeLintKind::DocTestTakesList,
431                        args.span().unwrap_or(path.span()),
432                    );
433                    return;
434                };
435
436                for i in list.mixed() {
437                    match i {
438                        MetaItemOrLitParser::MetaItemParser(mip) => {
439                            self.parse_single_test_doc_attr_item(cx, mip);
440                        }
441                        MetaItemOrLitParser::Lit(lit) => {
442                            cx.unexpected_literal(lit.span);
443                        }
444                        MetaItemOrLitParser::Err(..) => {
445                            // already had an error here, move on.
446                        }
447                    }
448                }
449            }
450            Some(sym::spotlight) => {
451                cx.emit_lint(
452                    rustc_session::lint::builtin::INVALID_DOC_ATTRIBUTES,
453                    AttributeLintKind::DocUnknownSpotlight { span: path.span() },
454                    path.span(),
455                );
456            }
457            Some(sym::include) if let Some(nv) = args.name_value() => {
458                let inner = match cx.attr_style {
459                    AttrStyle::Outer => "",
460                    AttrStyle::Inner => "!",
461                };
462                cx.emit_lint(
463                    rustc_session::lint::builtin::INVALID_DOC_ATTRIBUTES,
464                    AttributeLintKind::DocUnknownInclude {
465                        inner,
466                        value: nv.value_as_lit().symbol,
467                        span: path.span(),
468                    },
469                    path.span(),
470                );
471            }
472            Some(name @ (sym::passes | sym::no_default_passes)) => {
473                cx.emit_lint(
474                    rustc_session::lint::builtin::INVALID_DOC_ATTRIBUTES,
475                    AttributeLintKind::DocUnknownPasses { name, span: path.span() },
476                    path.span(),
477                );
478            }
479            Some(sym::plugins) => {
480                cx.emit_lint(
481                    rustc_session::lint::builtin::INVALID_DOC_ATTRIBUTES,
482                    AttributeLintKind::DocUnknownPlugins { span: path.span() },
483                    path.span(),
484                );
485            }
486            Some(name) => {
487                cx.emit_lint(
488                    rustc_session::lint::builtin::INVALID_DOC_ATTRIBUTES,
489                    AttributeLintKind::DocUnknownAny { name },
490                    path.span(),
491                );
492            }
493            None => {
494                let full_name =
495                    path.segments().map(|s| s.as_str()).intersperse("::").collect::<String>();
496                cx.emit_lint(
497                    rustc_session::lint::builtin::INVALID_DOC_ATTRIBUTES,
498                    AttributeLintKind::DocUnknownAny { name: Symbol::intern(&full_name) },
499                    path.span(),
500                );
501            }
502        }
503    }
504
505    fn accept_single_doc_attr<S: Stage>(
506        &mut self,
507        cx: &mut AcceptContext<'_, '_, S>,
508        args: &ArgParser,
509    ) {
510        match args {
511            ArgParser::NoArgs => {
512                let suggestions = cx.suggestions();
513                let span = cx.attr_span;
514                cx.emit_lint(
515                    rustc_session::lint::builtin::ILL_FORMED_ATTRIBUTE_INPUT,
516                    AttributeLintKind::IllFormedAttributeInput { suggestions, docs: None },
517                    span,
518                );
519            }
520            ArgParser::List(items) => {
521                for i in items.mixed() {
522                    match i {
523                        MetaItemOrLitParser::MetaItemParser(mip) => {
524                            self.nb_doc_attrs += 1;
525                            self.parse_single_doc_attr_item(cx, mip);
526                        }
527                        MetaItemOrLitParser::Lit(lit) => {
528                            cx.expected_name_value(lit.span, None);
529                        }
530                        MetaItemOrLitParser::Err(..) => {
531                            // already had an error here, move on.
532                        }
533                    }
534                }
535            }
536            ArgParser::NameValue(nv) => {
537                if nv.value_as_str().is_none() {
538                    cx.expected_string_literal(nv.value_span, Some(nv.value_as_lit()));
539                } else {
540                    unreachable!(
541                        "Should have been handled at the same time as sugar-syntaxed doc comments"
542                    );
543                }
544            }
545        }
546    }
547}
548
549impl<S: Stage> AttributeParser<S> for DocParser {
550    const ATTRIBUTES: AcceptMapping<Self, S> = &[(
551        &[sym::doc],
552        template!(
553            List: &[
554                "alias",
555                "attribute",
556                "hidden",
557                "html_favicon_url",
558                "html_logo_url",
559                "html_no_source",
560                "html_playground_url",
561                "html_root_url",
562                "issue_tracker_base_url",
563                "inline",
564                "no_inline",
565                "masked",
566                "cfg",
567                "notable_trait",
568                "keyword",
569                "fake_variadic",
570                "search_unbox",
571                "rust_logo",
572                "auto_cfg",
573                "test",
574                "spotlight",
575                "include",
576                "no_default_passes",
577                "passes",
578                "plugins",
579            ],
580            NameValueStr: "string"
581        ),
582        |this, cx, args| {
583            this.accept_single_doc_attr(cx, args);
584        },
585    )];
586    // FIXME: Currently emitted from 2 different places, generating duplicated warnings.
587    const ALLOWED_TARGETS: AllowedTargets = AllowedTargets::AllowList(ALL_TARGETS);
588    // const ALLOWED_TARGETS: AllowedTargets = AllowedTargets::AllowListWarnRest(&[
589    //     Allow(Target::ExternCrate),
590    //     Allow(Target::Use),
591    //     Allow(Target::Static),
592    //     Allow(Target::Const),
593    //     Allow(Target::Fn),
594    //     Allow(Target::Mod),
595    //     Allow(Target::ForeignMod),
596    //     Allow(Target::TyAlias),
597    //     Allow(Target::Enum),
598    //     Allow(Target::Variant),
599    //     Allow(Target::Struct),
600    //     Allow(Target::Field),
601    //     Allow(Target::Union),
602    //     Allow(Target::Trait),
603    //     Allow(Target::TraitAlias),
604    //     Allow(Target::Impl { of_trait: true }),
605    //     Allow(Target::Impl { of_trait: false }),
606    //     Allow(Target::AssocConst),
607    //     Allow(Target::Method(MethodKind::Inherent)),
608    //     Allow(Target::Method(MethodKind::Trait { body: true })),
609    //     Allow(Target::Method(MethodKind::Trait { body: false })),
610    //     Allow(Target::Method(MethodKind::TraitImpl)),
611    //     Allow(Target::AssocTy),
612    //     Allow(Target::ForeignFn),
613    //     Allow(Target::ForeignStatic),
614    //     Allow(Target::ForeignTy),
615    //     Allow(Target::MacroDef),
616    //     Allow(Target::Crate),
617    //     Error(Target::WherePredicate),
618    // ]);
619
620    fn finalize(self, _cx: &FinalizeContext<'_, '_, S>) -> Option<AttributeKind> {
621        if self.nb_doc_attrs != 0 {
622            Some(AttributeKind::Doc(Box::new(self.attribute)))
623        } else {
624            None
625        }
626    }
627}