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 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 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 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 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 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 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 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 }
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 }
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 const ALLOWED_TARGETS: AllowedTargets = AllowedTargets::AllowList(ALL_TARGETS);
588 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}