1use clippy_config::Conf;2use clippy_utils::attrs::is_doc_hidden;3use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_then};4use clippy_utils::macros::root_macro_call;5use clippy_utils::{is_entrypoint_fn, is_trait_impl_item, sym};6use rustc_ast::token::DocFragmentKind;7use rustc_attr_ir::Attribute;8use rustc_data_structures::fx::FxHashSet;9use rustc_errors::Applicability;10use rustc_hir::{FieldDef, ImplItemKind, ItemKind, Node, Safety, TraitItemKind};11use rustc_lint::{EarlyContext, EarlyLintPass, LateContext, LateLintPass, LintContext as _, impl_lint_pass};12use rustc_resolve::rustdoc::pulldown_cmark::Event::{13 Code, DisplayMath, End, FootnoteReference, HardBreak, Html, InlineHtml, InlineMath, Rule, SoftBreak, Start,14 TaskListMarker, Text,15};16use rustc_resolve::rustdoc::pulldown_cmark::Tag::{17 BlockQuote, CodeBlock, FootnoteDefinition, Heading, Item, Link, Paragraph,18};19use rustc_resolve::rustdoc::pulldown_cmark::{BrokenLink, CodeBlockKind, CowStr, Options, TagEnd};20use rustc_resolve::rustdoc::{21 DocFragment, add_doc_fragment, attrs_to_doc_fragments, main_body_opts, pulldown_cmark,22 source_span_for_markdown_range, span_of_fragments,23};24use rustc_span::Span;25use std::ops::Range;26use url::Url;2728mod broken_link;29mod doc_comment_double_space_linebreaks;30mod doc_paragraphs_missing_punctuation;31mod doc_suspicious_footnotes;32mod include_in_doc_without_cfg;33mod lazy_continuation;34mod link_with_quotes;35mod markdown;36mod missing_headers;37mod needless_doctest_main;38mod suspicious_doc_comments;39mod test_attr_in_doctest;40mod too_long_first_doc_paragraph;4142declare_clippy_lint! {43 /// ### What it does44 /// Checks the doc comments have unbroken links, mostly caused45 /// by bad formatted links such as broken across multiple lines.46 ///47 /// ### Why is this bad?48 /// Because documentation generated by rustdoc will be broken49 /// since expected links won't be links and just text.50 ///51 /// ### Examples52 /// This link is broken:53 /// ```no_run54 /// /// [example of a bad link](https://55 /// /// github.com/rust-lang/rust-clippy/)56 /// pub fn do_something() {}57 /// ```58 ///59 /// It shouldn't be broken across multiple lines to work:60 /// ```no_run61 /// /// [example of a good link](https://github.com/rust-lang/rust-clippy/)62 /// pub fn do_something() {}63 /// ```64 #[clippy::version = "1.90.0"]65 pub DOC_BROKEN_LINK,66 pedantic,67 "broken document link"68}6970declare_clippy_lint! {71 /// ### What it does72 /// Detects doc comments that use double spaces as hard line break, instead of backslash (`\`).73 ///74 /// ### Why is this bad?75 /// Double spaces, when used as hard line break in doc comments, can be difficult to see, and may76 /// accidentally be removed during automatic formatting or manual refactoring. The use of a backslash (`\`)77 /// is clearer in this regard.78 ///79 /// ### Example80 /// The two replacement dots (`··`) in this example represent a double space.81 /// ```no_run82 /// /// This function adds two numbers and returns the result··83 /// /// Overflow can occur when the max value is exceeded.84 /// fn add(l: i32, r: i32) -> i32 {85 /// l + r86 /// }87 /// ```88 ///89 /// Use instead:90 /// ```no_run91 /// /// This function adds two numbers and returns the result\92 /// /// Overflow can occur when the max value is exceeded.93 /// fn add(l: i32, r: i32) -> i32 {94 /// l + r95 /// }96 /// ```97 #[clippy::version = "1.87.0"]98 pub DOC_COMMENT_DOUBLE_SPACE_LINEBREAKS,99 pedantic,100 "double space used for doc comment hard line break instead of `\\`"101}102103declare_clippy_lint! {104 /// ### What it does105 /// Checks if included files in doc comments are included only for `cfg(doc)`.106 ///107 /// ### Why restrict this?108 /// These files are not useful for compilation but will still be included.109 /// Also, if any of these non-source code file is updated, it will trigger a110 /// recompilation.111 ///112 /// ### Known problems113 ///114 /// Excluding this will currently result in the file being left out if115 /// the item's docs are inlined from another crate. This may be fixed in a116 /// future version of rustdoc.117 ///118 /// ### Example119 /// ```ignore120 /// #![doc = include_str!("some_file.md")]121 /// ```122 /// Use instead:123 /// ```no_run124 /// #![cfg_attr(doc, doc = include_str!("some_file.md"))]125 /// ```126 #[clippy::version = "1.85.0"]127 pub DOC_INCLUDE_WITHOUT_CFG,128 restriction,129 "check if files included in documentation are behind `cfg(doc)`"130}131132declare_clippy_lint! {133 /// ### What it does134 ///135 /// In CommonMark Markdown, the language used to write doc comments, a136 /// paragraph nested within a list or block quote does not need any line137 /// after the first one to be indented or marked. The specification calls138 /// this a "lazy paragraph continuation."139 ///140 /// ### Why is this bad?141 ///142 /// This is easy to write but hard to read. Lazy continuations makes143 /// unintended markers hard to see, and make it harder to deduce the144 /// document's intended structure.145 ///146 /// ### Example147 ///148 /// This table is probably intended to have two rows,149 /// but it does not. It has zero rows, and is followed by150 /// a block quote.151 /// ```no_run152 /// /// Range | Description153 /// /// ----- | -----------154 /// /// >= 1 | fully opaque155 /// /// < 1 | partially see-through156 /// fn set_opacity(opacity: f32) {}157 /// ```158 ///159 /// Fix it by escaping the marker:160 /// ```no_run161 /// /// Range | Description162 /// /// ----- | -----------163 /// /// \>= 1 | fully opaque164 /// /// < 1 | partially see-through165 /// fn set_opacity(opacity: f32) {}166 /// ```167 ///168 /// This example is actually intended to be a list:169 /// ```no_run170 /// /// * Do nothing.171 /// /// * Then do something. Whatever it is needs done,172 /// /// it should be done right now.173 /// # fn do_stuff() {}174 /// ```175 ///176 /// Fix it by indenting the list contents:177 /// ```no_run178 /// /// * Do nothing.179 /// /// * Then do something. Whatever it is needs done,180 /// /// it should be done right now.181 /// # fn do_stuff() {}182 /// ```183 #[clippy::version = "1.80.0"]184 pub DOC_LAZY_CONTINUATION,185 style,186 "require every line of a paragraph to be indented and marked"187}188189declare_clippy_lint! {190 /// ### What it does191 /// Checks for links with code directly adjacent to code text:192 /// `` [`MyItem`]`<`[`u32`]`>` ``.193 ///194 /// ### Why is this bad?195 /// It can be written more simply using HTML-style `<code>` tags.196 ///197 /// ### Example198 /// ```no_run199 /// //! [`first`](x)`second`200 /// ```201 /// Use instead:202 /// ```no_run203 /// //! <code>[first](x)second</code>204 /// ```205 #[clippy::version = "1.87.0"]206 pub DOC_LINK_CODE,207 nursery,208 "link with code back-to-back with other code"209}210211declare_clippy_lint! {212 /// ### What it does213 /// Detects the syntax `['foo']` in documentation comments (notice quotes instead of backticks)214 /// outside of code blocks215 /// ### Why is this bad?216 /// It is likely a typo when defining an intra-doc link217 ///218 /// ### Example219 /// ```no_run220 /// /// See also: ['foo']221 /// fn bar() {}222 /// ```223 /// Use instead:224 /// ```no_run225 /// /// See also: [`foo`]226 /// fn bar() {}227 /// ```228 #[clippy::version = "1.63.0"]229 pub DOC_LINK_WITH_QUOTES,230 pedantic,231 "possible typo for an intra-doc link"232}233234declare_clippy_lint! {235 /// ### What it does236 /// Checks for the presence of `_`, `::` or camel-case words237 /// outside ticks in documentation.238 ///239 /// ### Why is this bad?240 /// *Rustdoc* supports markdown formatting, `_`, `::` and241 /// camel-case probably indicates some code which should be included between242 /// ticks. `_` can also be used for emphasis in markdown, this lint tries to243 /// consider that.244 ///245 /// ### Known problems246 /// Lots of bad docs won’t be fixed, what the lint checks247 /// for is limited, and there are still false positives. HTML elements and their248 /// content are not linted.249 ///250 /// In addition, when writing documentation comments, including `[]` brackets251 /// inside a link text would trip the parser. Therefore, documenting link with252 /// `[`SmallVec<[T; INLINE_CAPACITY]>`]` and then [`SmallVec<[T; INLINE_CAPACITY]>`]: SmallVec253 /// would fail.254 ///255 /// ### Examples256 /// ```no_run257 /// /// Do something with the foo_bar parameter. See also258 /// /// that::other::module::foo.259 /// // ^ `foo_bar` and `that::other::module::foo` should be ticked.260 /// fn doit(foo_bar: usize) {}261 /// ```262 ///263 /// ```no_run264 /// // Link text with `[]` brackets should be written as following:265 /// /// Consume the array and return the inner266 /// /// [`SmallVec<[T; INLINE_CAPACITY]>`][SmallVec].267 /// /// [SmallVec]: SmallVec268 /// fn foo() {}269 /// ```270 #[clippy::version = "pre 1.29.0"]271 pub DOC_MARKDOWN,272 pedantic,273 "presence of `_`, `::` or camel-case outside backticks in documentation"274}275276declare_clippy_lint! {277 /// ### What it does278 /// Warns if a link reference definition appears at the start of a279 /// list item or quote.280 ///281 /// ### Why is this bad?282 /// This is probably intended as an intra-doc link. If it is really283 /// supposed to be a reference definition, it can be written outside284 /// of the list item or quote.285 ///286 /// ### Example287 /// ```no_run288 /// //! - [link]: description289 /// ```290 /// Use instead:291 /// ```no_run292 /// //! - [link][]: description (for intra-doc link)293 /// //!294 /// //! [link]: destination (for link reference definition)295 /// ```296 #[clippy::version = "1.85.0"]297 pub DOC_NESTED_REFDEFS,298 suspicious,299 "link reference defined in list item or quote"300}301302declare_clippy_lint! {303 /// ### What it does304 ///305 /// Detects overindented list items in doc comments where the continuation306 /// lines are indented more than necessary.307 ///308 /// ### Why is this bad?309 ///310 /// Overindented list items in doc comments can lead to inconsistent and311 /// poorly formatted documentation when rendered. Excessive indentation may312 /// cause the text to be misinterpreted as a nested list item or code block,313 /// affecting readability and the overall structure of the documentation.314 ///315 /// ### Example316 ///317 /// ```no_run318 /// /// - This is the first item in a list319 /// /// and this line is overindented.320 /// # fn foo() {}321 /// ```322 ///323 /// Fixes this into:324 /// ```no_run325 /// /// - This is the first item in a list326 /// /// and this line is overindented.327 /// # fn foo() {}328 /// ```329 #[clippy::version = "1.86.0"]330 pub DOC_OVERINDENTED_LIST_ITEMS,331 style,332 "ensure list items are not overindented"333}334335declare_clippy_lint! {336 /// ### What it does337 /// Checks for doc comments whose paragraphs do not end with a period or another punctuation mark.338 /// Various Markdowns constructs are taken into account to avoid false positives.339 ///340 /// ### Why is this bad?341 /// A project may wish to enforce consistent doc comments by making sure paragraphs end with a342 /// punctuation mark.343 ///344 /// ### Example345 /// ```no_run346 /// /// Returns a random number347 /// ///348 /// /// It was chosen by a fair dice roll349 /// # fn foo() {}350 /// ```351 /// Use instead:352 /// ```no_run353 /// /// Returns a random number.354 /// ///355 /// /// It was chosen by a fair dice roll.356 /// # fn foo() {}357 /// ```358 ///359 /// ### Terminal punctuation marks360 /// This lint treats these characters as end markers: '.', '?', '!', '…' and ':'.361 ///362 /// The colon is not exactly a terminal punctuation mark, but this is required for paragraphs that363 /// introduce a table or a list for example.364 #[clippy::version = "1.93.0"]365 pub DOC_PARAGRAPHS_MISSING_PUNCTUATION,366 restriction,367 "missing terminal punctuation in doc comments"368}369370declare_clippy_lint! {371 /// ### What it does372 /// Detects syntax that looks like a footnote reference.373 ///374 /// Rustdoc footnotes are compatible with GitHub-Flavored Markdown (GFM).375 /// GFM does not parse a footnote reference unless its definition also376 /// exists. This lint checks for footnote references with missing377 /// definitions, unless it thinks you're writing a regex.378 ///379 /// ### Why is this bad?380 /// This probably means that a footnote was meant to exist,381 /// but was not written.382 ///383 /// ### Example384 /// ```no_run385 /// /// This is not a footnote[^1], because no definition exists.386 /// fn my_fn() {}387 /// ```388 /// Use instead:389 /// ```no_run390 /// /// This is a footnote[^1].391 /// ///392 /// /// [^1]: defined here393 /// fn my_fn() {}394 /// ```395 #[clippy::version = "1.89.0"]396 pub DOC_SUSPICIOUS_FOOTNOTES,397 suspicious,398 "looks like a link or footnote ref, but with no definition"399}400401declare_clippy_lint! {402 /// ### What it does403 /// Detects documentation that is empty.404 /// ### Why is this bad?405 /// Empty docs clutter code without adding value, reducing readability and maintainability.406 /// ### Example407 /// ```no_run408 /// ///409 /// fn returns_true() -> bool {410 /// true411 /// }412 /// ```413 /// Use instead:414 /// ```no_run415 /// fn returns_true() -> bool {416 /// true417 /// }418 /// ```419 #[clippy::version = "1.78.0"]420 pub EMPTY_DOCS,421 suspicious,422 "docstrings exist but documentation is empty"423}424425declare_clippy_lint! {426 /// ### What it does427 /// Checks the doc comments of publicly visible functions that428 /// return a `Result` type and warns if there is no `# Errors` section.429 ///430 /// ### Why is this bad?431 /// Documenting the type of errors that can be returned from a432 /// function can help callers write code to handle the errors appropriately.433 ///434 /// ### Examples435 /// Since the following function returns a `Result` it has an `# Errors` section in436 /// its doc comment:437 ///438 /// ```no_run439 ///# use std::io;440 /// /// # Errors441 /// ///442 /// /// Will return `Err` if `filename` does not exist or the user does not have443 /// /// permission to read it.444 /// pub fn read(filename: String) -> io::Result<String> {445 /// unimplemented!();446 /// }447 /// ```448 #[clippy::version = "1.41.0"]449 pub MISSING_ERRORS_DOC,450 pedantic,451 "`pub fn` returns `Result` without `# Errors` in doc comment"452}453454declare_clippy_lint! {455 /// ### What it does456 /// Checks the doc comments of publicly visible functions that457 /// may panic and warns if there is no `# Panics` section.458 ///459 /// ### Why is this bad?460 /// Documenting the scenarios in which panicking occurs461 /// can help callers who do not want to panic to avoid those situations.462 ///463 /// ### Examples464 /// Since the following function may panic it has a `# Panics` section in465 /// its doc comment:466 ///467 /// ```no_run468 /// /// # Panics469 /// ///470 /// /// Will panic if y is 0471 /// pub fn divide_by(x: i32, y: i32) -> i32 {472 /// if y == 0 {473 /// panic!("Cannot divide by 0")474 /// } else {475 /// x / y476 /// }477 /// }478 /// ```479 ///480 /// Individual panics within a function can be ignored with `#[expect]` or481 /// `#[allow]`:482 ///483 /// ```no_run484 /// # use std::num::NonZeroUsize;485 /// pub fn will_not_panic(x: usize) {486 /// #[expect(clippy::missing_panics_doc, reason = "infallible")]487 /// let y = NonZeroUsize::new(1).unwrap();488 ///489 /// // If any panics are added in the future the lint will still catch them490 /// }491 /// ```492 #[clippy::version = "1.51.0"]493 pub MISSING_PANICS_DOC,494 pedantic,495 "`pub fn` may panic without `# Panics` in doc comment"496}497498declare_clippy_lint! {499 /// ### What it does500 /// Checks for the doc comments of publicly visible501 /// unsafe functions and warns if there is no `# Safety` section.502 ///503 /// ### Why is this bad?504 /// Unsafe functions should document their safety505 /// preconditions, so that users can be sure they are using them safely.506 ///507 /// ### Examples508 /// ```no_run509 ///# type Universe = ();510 /// /// This function should really be documented511 /// pub unsafe fn start_apocalypse(u: &mut Universe) {512 /// unimplemented!();513 /// }514 /// ```515 ///516 /// At least write a line about safety:517 ///518 /// ```no_run519 ///# type Universe = ();520 /// /// # Safety521 /// ///522 /// /// This function should not be called before the horsemen are ready.523 /// pub unsafe fn start_apocalypse(u: &mut Universe) {524 /// unimplemented!();525 /// }526 /// ```527 #[clippy::version = "1.39.0"]528 pub MISSING_SAFETY_DOC,529 style,530 "`pub unsafe fn` without `# Safety` docs"531}532533declare_clippy_lint! {534 /// ### What it does535 /// Checks for `fn main() { .. }` in doctests536 ///537 /// ### Why is this bad?538 /// The test can be shorter (and likely more readable)539 /// if the `fn main()` is left implicit.540 ///541 /// ### Examples542 /// ```no_run543 /// /// An example of a doctest with a `main()` function544 /// ///545 /// /// # Examples546 /// ///547 /// /// ```548 /// /// fn main() {549 /// /// // this needs not be in an `fn`550 /// /// }551 /// /// ```552 /// fn needless_main() {553 /// unimplemented!();554 /// }555 /// ```556 #[clippy::version = "1.40.0"]557 pub NEEDLESS_DOCTEST_MAIN,558 style,559 "presence of `fn main() {` in code examples"560}561562declare_clippy_lint! {563 /// ### What it does564 /// Detects the use of outer doc comments (`///`, `/**`) followed by a bang (`!`): `///!`565 ///566 /// ### Why is this bad?567 /// Triple-slash comments (known as "outer doc comments") apply to items that follow it.568 /// An outer doc comment followed by a bang (i.e. `///!`) has no specific meaning.569 ///570 /// The user most likely meant to write an inner doc comment (`//!`, `/*!`), which571 /// applies to the parent item (i.e. the item that the comment is contained in,572 /// usually a module or crate).573 ///574 /// ### Known problems575 /// Inner doc comments can only appear before items, so there are certain cases where the suggestion576 /// made by this lint is not valid code. For example:577 /// ```rust578 /// fn foo() {}579 /// ///!580 /// fn bar() {}581 /// ```582 /// This lint detects the doc comment and suggests changing it to `//!`, but an inner doc comment583 /// is not valid at that position.584 ///585 /// ### Example586 /// In this example, the doc comment is attached to the *function*, rather than the *module*.587 /// ```no_run588 /// pub mod util {589 /// ///! This module contains utility functions.590 ///591 /// pub fn dummy() {}592 /// }593 /// ```594 ///595 /// Use instead:596 /// ```no_run597 /// pub mod util {598 /// //! This module contains utility functions.599 ///600 /// pub fn dummy() {}601 /// }602 /// ```603 #[clippy::version = "1.70.0"]604 pub SUSPICIOUS_DOC_COMMENTS,605 suspicious,606 "suspicious usage of (outer) doc comments"607}608609declare_clippy_lint! {610 /// ### What it does611 /// Checks for `#[test]` in doctests unless they are marked with612 /// either `ignore`, `no_run` or `compile_fail`.613 ///614 /// ### Why is this bad?615 /// Code in examples marked as `#[test]` will somewhat616 /// surprisingly not be run by `cargo test`. If you really want617 /// to show how to test stuff in an example, mark it `no_run` to618 /// make the intent clear.619 ///620 /// ### Examples621 /// ```no_run622 /// /// An example of a doctest with a `main()` function623 /// ///624 /// /// # Examples625 /// ///626 /// /// ```627 /// /// #[test]628 /// /// fn equality_works() {629 /// /// assert_eq!(1_u8, 1);630 /// /// }631 /// /// ```632 /// fn test_attr_in_doctest() {633 /// unimplemented!();634 /// }635 /// ```636 #[clippy::version = "1.76.0"]637 pub TEST_ATTR_IN_DOCTEST,638 suspicious,639 "presence of `#[test]` in code examples"640}641642declare_clippy_lint! {643 /// ### What it does644 /// Checks if the first paragraph in the documentation of items listed in the module page is too long.645 ///646 /// ### Why is this bad?647 /// Documentation will show the first paragraph of the docstring in the summary page of a648 /// module. Having a nice, short summary in the first paragraph is part of writing good docs.649 ///650 /// ### Example651 /// ```no_run652 /// /// A very short summary.653 /// /// A much longer explanation that goes into a lot more detail about654 /// /// how the thing works, possibly with doclinks and so one,655 /// /// and probably spanning a many rows.656 /// struct Foo {}657 /// ```658 /// Use instead:659 /// ```no_run660 /// /// A very short summary.661 /// ///662 /// /// A much longer explanation that goes into a lot more detail about663 /// /// how the thing works, possibly with doclinks and so one,664 /// /// and probably spanning a many rows.665 /// struct Foo {}666 /// ```667 #[clippy::version = "1.82.0"]668 pub TOO_LONG_FIRST_DOC_PARAGRAPH,669 nursery,670 "ensure the first documentation paragraph is short"671}672673declare_clippy_lint! {674 /// ### What it does675 /// Checks for the doc comments of publicly visible676 /// safe functions and traits and warns if there is a `# Safety` section.677 ///678 /// ### Why restrict this?679 /// Safe functions and traits are safe to implement and therefore do not680 /// need to describe safety preconditions that users are required to uphold.681 ///682 /// ### Examples683 /// ```no_run684 ///# type Universe = ();685 /// /// # Safety686 /// ///687 /// /// This function should not be called before the horsemen are ready.688 /// pub fn start_apocalypse_but_safely(u: &mut Universe) {689 /// unimplemented!();690 /// }691 /// ```692 ///693 /// The function is safe, so there shouldn't be any preconditions694 /// that have to be explained for safety reasons.695 ///696 /// ```no_run697 ///# type Universe = ();698 /// /// This function should really be documented699 /// pub fn start_apocalypse(u: &mut Universe) {700 /// unimplemented!();701 /// }702 /// ```703 #[clippy::version = "1.67.0"]704 pub UNNECESSARY_SAFETY_DOC,705 restriction,706 "`pub fn` or `pub trait` with `# Safety` docs"707}708709impl_lint_pass!(Documentation => [710 DOC_BROKEN_LINK,711 DOC_COMMENT_DOUBLE_SPACE_LINEBREAKS,712 DOC_INCLUDE_WITHOUT_CFG,713 DOC_LAZY_CONTINUATION,714 DOC_LINK_CODE,715 DOC_LINK_WITH_QUOTES,716 DOC_MARKDOWN,717 DOC_NESTED_REFDEFS,718 DOC_OVERINDENTED_LIST_ITEMS,719 DOC_PARAGRAPHS_MISSING_PUNCTUATION,720 DOC_SUSPICIOUS_FOOTNOTES,721 EMPTY_DOCS,722 MISSING_ERRORS_DOC,723 MISSING_PANICS_DOC,724 MISSING_SAFETY_DOC,725 NEEDLESS_DOCTEST_MAIN,726 SUSPICIOUS_DOC_COMMENTS,727 TEST_ATTR_IN_DOCTEST,728 TOO_LONG_FIRST_DOC_PARAGRAPH,729 UNNECESSARY_SAFETY_DOC,730]);731732pub struct Documentation {733 valid_idents: &'static FxHashSet<String>,734 check_private_items: bool,735}736737impl Documentation {738 pub fn new(conf: &'static Conf) -> Self {739 Self {740 valid_idents: &conf.doc_valid_idents,741 check_private_items: conf.check_private_items,742 }743 }744}745746impl EarlyLintPass for Documentation {747 fn check_attributes(&mut self, cx: &EarlyContext<'_>, attrs: &[rustc_ast::Attribute]) {748 include_in_doc_without_cfg::check(cx, attrs);749 }750}751752impl<'tcx> LateLintPass<'tcx> for Documentation {753 fn check_attributes(&mut self, cx: &LateContext<'tcx>, attrs: &'tcx [Attribute]) {754 let Some(headers) = check_attrs(cx, self.valid_idents, attrs) else {755 return;756 };757758 match cx.tcx.hir_node(cx.last_node_with_lint_attrs) {759 Node::Field(FieldDef { span, safety, .. }) => match (headers.safety, safety) {760 (false, Safety::Unsafe) => span_lint(761 cx,762 MISSING_SAFETY_DOC,763 *span,764 "docs for unsafe field missing `# Safety` section",765 ),766 (true, Safety::Safe) if cx.tcx.features().unsafe_fields() => span_lint_and_help(767 cx,768 UNNECESSARY_SAFETY_DOC,769 *span,770 "field with `# Safety` documentation is not marked unsafe",771 None,772 "if the field has safety invariants, mark it `unsafe`",773 ),774 _ => (),775 },776 Node::Item(item) => {777 too_long_first_doc_paragraph::check(778 cx,779 item,780 attrs,781 headers.first_paragraph_text_len,782 headers.first_paragraph_md_len,783 self.check_private_items,784 );785 match item.kind {786 ItemKind::Fn { sig, body, .. }787 if !(is_entrypoint_fn(cx, item.owner_id.to_def_id())788 || item.span.in_external_macro(cx.tcx.sess.source_map())) =>789 {790 missing_headers::check(cx, item.owner_id, sig, headers, Some(body), self.check_private_items);791 },792 ItemKind::Trait { safety, .. } => match (headers.safety, safety) {793 (false, Safety::Unsafe) => span_lint(794 cx,795 MISSING_SAFETY_DOC,796 cx.tcx.def_span(item.owner_id),797 "docs for unsafe trait missing `# Safety` section",798 ),799 (true, Safety::Safe) => span_lint(800 cx,801 UNNECESSARY_SAFETY_DOC,802 cx.tcx.def_span(item.owner_id),803 "docs for safe trait have unnecessary `# Safety` section",804 ),805 _ => (),806 },807 _ => (),808 }809 },810 Node::TraitItem(trait_item) => {811 if let TraitItemKind::Fn(sig, ..) = trait_item.kind812 && !trait_item.span.in_external_macro(cx.tcx.sess.source_map())813 {814 missing_headers::check(cx, trait_item.owner_id, sig, headers, None, self.check_private_items);815 }816 },817 Node::ImplItem(impl_item) => {818 if let ImplItemKind::Fn(sig, body_id) = impl_item.kind819 && !impl_item.span.in_external_macro(cx.tcx.sess.source_map())820 && !is_trait_impl_item(cx, impl_item.hir_id())821 {822 missing_headers::check(823 cx,824 impl_item.owner_id,825 sig,826 headers,827 Some(body_id),828 self.check_private_items,829 );830 }831 },832 _ => {},833 }834 }835}836837#[derive(Copy, Clone)]838struct Fragments<'a> {839 doc: &'a str,840 fragments: &'a [DocFragment],841}842843impl Fragments<'_> {844 /// get the span for the markdown range. Note that this function is not cheap, use it with845 /// caution.846 #[must_use]847 fn span(self, cx: &LateContext<'_>, range: Range<usize>) -> Option<Span> {848 source_span_for_markdown_range(cx.tcx, self.doc, &range, self.fragments).map(|(sp, _)| sp)849 }850}851852#[derive(Copy, Clone, Default)]853struct DocHeaders {854 safety: bool,855 errors: bool,856 panics: bool,857 first_paragraph_md_len: usize,858 first_paragraph_text_len: usize,859}860861/// Does some pre-processing on raw, desugared `#[doc]` attributes such as parsing them and862/// then delegates to `check_doc`.863/// Some lints are already checked here if they can work with attributes directly and don't need864/// to work with markdown.865/// Others are checked elsewhere, e.g. in `check_doc` if they need access to markdown, or866/// back in the various late lint pass methods if they need the final doc headers, like "Safety" or867/// "Panics" sections.868fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[Attribute]) -> Option<DocHeaders> {869 // We don't want the parser to choke on intra doc links. Since we don't870 // actually care about rendering them, just pretend that all broken links871 // point to a fake address.872 #[expect(clippy::unnecessary_wraps)] // we're following a type signature873 fn fake_broken_link_callback<'a>(_: BrokenLink<'_>) -> Option<(CowStr<'a>, CowStr<'a>)> {874 Some(("fake".into(), "fake".into()))875 }876877 if suspicious_doc_comments::check(cx, attrs) || is_doc_hidden(attrs) {878 return None;879 }880881 let (fragments, _) = attrs_to_doc_fragments(882 attrs.iter().filter_map(|attr| {883 let (_, kind) = attr.doc_str_and_fragment_kind()?;884 let (is_raw, doc_str_span) = match kind {885 DocFragmentKind::Sugared(_) => (false, attr.span()),886 DocFragmentKind::Raw(span) => (true, span),887 };888 let from_include_str_macro = is_raw889 && !attr.span().in_external_macro(cx.sess().source_map())890 && originated_from_include_str_macro(cx, doc_str_span);891 (from_include_str_macro || !doc_str_span.in_external_macro(cx.sess().source_map())).then_some((attr, None))892 }),893 true,894 );895896 let mut doc = String::with_capacity(fragments.iter().map(|frag| frag.doc.as_str().len() + 1).sum());897898 for fragment in &fragments {899 add_doc_fragment(&mut doc, fragment);900 }901 doc.pop();902903 if doc.trim().is_empty() {904 if let Some(span) = span_of_fragments(&fragments) {905 span_lint_and_help(906 cx,907 EMPTY_DOCS,908 span,909 "empty doc comment",910 None,911 "consider removing or filling it",912 );913 }914 return Some(DocHeaders::default());915 }916917 // Only emits the allow-by-default `DOC_LINK_CODE`; skip its extra markdown reparse when it's off.918 if !clippy_utils::is_lint_allowed(cx, DOC_LINK_CODE, cx.last_node_with_lint_attrs) {919 check_for_code_clusters(920 cx,921 pulldown_cmark::Parser::new_with_broken_link_callback(922 &doc,923 main_body_opts() - Options::ENABLE_SMART_PUNCTUATION,924 Some(&mut fake_broken_link_callback),925 )926 .into_offset_iter(),927 &doc,928 Fragments {929 doc: &doc,930 fragments: &fragments,931 },932 );933 }934935 // Same for the allow-by-default `DOC_PARAGRAPHS_MISSING_PUNCTUATION`, which also reparses.936 if !clippy_utils::is_lint_allowed(cx, DOC_PARAGRAPHS_MISSING_PUNCTUATION, cx.last_node_with_lint_attrs) {937 doc_paragraphs_missing_punctuation::check(938 cx,939 &doc,940 Fragments {941 doc: &doc,942 fragments: &fragments,943 },944 );945 }946947 // NOTE: check_doc uses it own cb function,948 // to avoid causing duplicated diagnostics for the broken link checker.949 let mut full_fake_broken_link_callback = |bl: BrokenLink<'_>| -> Option<(CowStr<'_>, CowStr<'_>)> {950 broken_link::check(cx, &bl, &doc, &fragments);951 Some(("fake".into(), "fake".into()))952 };953954 // disable smart punctuation to pick up ['link'] more easily955 let opts = main_body_opts() - Options::ENABLE_SMART_PUNCTUATION;956 let parser =957 pulldown_cmark::Parser::new_with_broken_link_callback(&doc, opts, Some(&mut full_fake_broken_link_callback));958959 Some(check_doc(960 cx,961 valid_idents,962 parser.into_offset_iter(),963 &doc,964 Fragments {965 doc: &doc,966 fragments: &fragments,967 },968 attrs,969 ))970}971972fn originated_from_include_str_macro(cx: &LateContext<'_>, span: Span) -> bool {973 root_macro_call(span).is_some_and(|macro_call| cx.tcx.is_diagnostic_item(sym::include_str_macro, macro_call.def_id))974}975976enum Container {977 Blockquote,978 List(usize),979}980981/// Scan the documentation for code links that are back-to-back with code spans.982///983/// This is done separately from the rest of the docs, because that makes it easier to produce984/// the correct messages.985fn check_for_code_clusters<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize>)>>(986 cx: &LateContext<'_>,987 events: Events,988 doc: &str,989 fragments: Fragments<'_>,990) {991 let mut events = events.peekable();992 let mut code_starts_at = None;993 let mut code_ends_at = None;994 let mut code_includes_link = false;995 while let Some((event, range)) = events.next() {996 match event {997 Start(Link { .. }) if matches!(events.peek(), Some((Code(_), _range))) => {998 if code_starts_at.is_some() {999 code_ends_at = Some(range.end);1000 } else {1001 code_starts_at = Some(range.start);1002 }1003 code_includes_link = true;1004 // skip the nested "code", because we're already handling it here1005 let _ = events.next();1006 },1007 Code(_) => {1008 if code_starts_at.is_some() {1009 code_ends_at = Some(range.end);1010 } else {1011 code_starts_at = Some(range.start);1012 }1013 },1014 End(TagEnd::Link) => {},1015 _ => {1016 if let Some(start) = code_starts_at1017 && let Some(end) = code_ends_at1018 && code_includes_link1019 && let Some(span) = fragments.span(cx, start..end)1020 {1021 span_lint_and_then(cx, DOC_LINK_CODE, span, "code link adjacent to code text", |diag| {1022 let sugg = format!("<code>{}</code>", doc[start..end].replace('`', ""));1023 diag.span_suggestion_verbose(1024 span,1025 "wrap the entire group in `<code>` tags",1026 sugg,1027 Applicability::MaybeIncorrect,1028 );1029 diag.help("separate code snippets will be shown with a gap");1030 });1031 }1032 code_includes_link = false;1033 code_starts_at = None;1034 code_ends_at = None;1035 },1036 }1037 }1038}10391040#[derive(Clone, Copy)]1041#[expect(clippy::struct_excessive_bools)]1042struct CodeTags {1043 no_run: bool,1044 ignore: bool,1045 compile_fail: bool,1046 test_harness: bool,10471048 rust: bool,1049}10501051impl Default for CodeTags {1052 fn default() -> Self {1053 Self {1054 no_run: false,1055 ignore: false,1056 compile_fail: false,1057 test_harness: false,10581059 rust: true,1060 }1061 }1062}10631064impl CodeTags {1065 /// Based on <https://github.com/rust-lang/rust/blob/1.90.0/src/librustdoc/html/markdown.rs#L1169>1066 fn parse(lang: &str) -> Self {1067 let mut tags = Self::default();10681069 let mut seen_rust_tags = false;1070 let mut seen_other_tags = false;1071 for item in lang.split([',', ' ', '\t']) {1072 match item.trim() {1073 "" => {},1074 "rust" => {1075 tags.rust = true;1076 seen_rust_tags = true;1077 },1078 "ignore" => {1079 tags.ignore = true;1080 seen_rust_tags = !seen_other_tags;1081 },1082 "no_run" => {1083 tags.no_run = true;1084 seen_rust_tags = !seen_other_tags;1085 },1086 "should_panic" => seen_rust_tags = !seen_other_tags,1087 "compile_fail" => {1088 tags.compile_fail = true;1089 seen_rust_tags = !seen_other_tags || seen_rust_tags;1090 },1091 "test_harness" => {1092 tags.test_harness = true;1093 seen_rust_tags = !seen_other_tags || seen_rust_tags;1094 },1095 "standalone_crate" => {1096 seen_rust_tags = !seen_other_tags || seen_rust_tags;1097 },1098 _ if item.starts_with("ignore-") => seen_rust_tags = true,1099 _ if item.starts_with("edition") => {},1100 _ => seen_other_tags = true,1101 }1102 }11031104 tags.rust &= seen_rust_tags || !seen_other_tags;11051106 tags1107 }1108}11091110/// Checks parsed documentation.1111/// This walks the "events" (think sections of markdown) produced by `pulldown_cmark`,1112/// so lints here will generally access that information.1113/// Returns documentation headers -- whether a "Safety", "Errors", "Panic" section was found1114#[expect(clippy::too_many_lines, reason = "big match statement")]1115fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize>)>>(1116 cx: &LateContext<'_>,1117 valid_idents: &FxHashSet<String>,1118 events: Events,1119 doc: &str,1120 fragments: Fragments<'_>,1121 attrs: &[Attribute],1122) -> DocHeaders {1123 // true if a safety header was found1124 let mut headers = DocHeaders::default();1125 let mut code = None;1126 let mut in_link = None;1127 let mut in_heading = false;1128 let mut in_footnote_definition = false;1129 let mut ticks_unbalanced = false;1130 let mut text_to_check: Vec<(CowStr<'_>, Range<usize>, isize)> = Vec::new();1131 let mut paragraph_range = 0..0;1132 let mut code_level = 0;1133 let mut blockquote_level = 0;1134 let mut collected_breaks: Vec<Span> = Vec::new();1135 let mut is_first_paragraph = true;11361137 let mut containers = Vec::new();11381139 // Skip collecting text and the per-word scan when `DOC_MARKDOWN` (pedantic) is allowed.1140 let check_doc_markdown = !clippy_utils::is_lint_allowed(cx, DOC_MARKDOWN, cx.last_node_with_lint_attrs);11411142 let mut events = events.peekable();11431144 while let Some((event, range)) = events.next() {1145 match event {1146 Html(tag) | InlineHtml(tag) => {1147 if tag.starts_with("<code") {1148 code_level += 1;1149 } else if tag.starts_with("</code") {1150 code_level -= 1;1151 } else if tag.starts_with("<blockquote") || tag.starts_with("<q") {1152 blockquote_level += 1;1153 } else if tag.starts_with("</blockquote") || tag.starts_with("</q") {1154 blockquote_level -= 1;1155 }1156 },1157 Start(BlockQuote(_)) => {1158 blockquote_level += 1;1159 containers.push(Container::Blockquote);1160 if let Some((next_event, next_range)) = events.peek() {1161 let next_start = match next_event {1162 End(TagEnd::BlockQuote) => next_range.end,1163 _ => next_range.start,1164 };1165 if let Some(refdefrange) = looks_like_refdef(doc, range.start..next_start) &&1166 let Some(refdefspan) = fragments.span(cx, refdefrange.clone())1167 {1168 span_lint_and_then(1169 cx,1170 DOC_NESTED_REFDEFS,1171 refdefspan,1172 "link reference defined in quote",1173 |diag| {1174 diag.span_suggestion_short(1175 refdefspan.shrink_to_hi(),1176 "for an intra-doc link, add `[]` between the label and the colon",1177 "[]",1178 Applicability::MaybeIncorrect,1179 );1180 diag.help("link definitions are not shown in rendered documentation");1181 }1182 );1183 }1184 }1185 },1186 End(TagEnd::BlockQuote) => {1187 blockquote_level -= 1;1188 containers.pop();1189 },1190 Start(CodeBlock(ref kind)) => {1191 code = Some(match kind {1192 CodeBlockKind::Indented => CodeTags::default(),1193 CodeBlockKind::Fenced(lang) => CodeTags::parse(lang),1194 });1195 },1196 End(TagEnd::CodeBlock) => code = None,1197 Start(Link { dest_url, .. }) => in_link = Some(dest_url),1198 End(TagEnd::Link) => in_link = None,1199 Start(Heading { .. } | Paragraph | Item) => {1200 if let Start(Heading { .. }) = event {1201 in_heading = true;1202 }1203 if let Start(Item) = event {1204 let indent = if let Some((next_event, next_range)) = events.peek() {1205 let next_start = match next_event {1206 End(TagEnd::Item) => next_range.end,1207 _ => next_range.start,1208 };1209 if let Some(refdefrange) = looks_like_refdef(doc, range.start..next_start) &&1210 let Some(refdefspan) = fragments.span(cx, refdefrange.clone())1211 {1212 span_lint_and_then(1213 cx,1214 DOC_NESTED_REFDEFS,1215 refdefspan,1216 "link reference defined in list item",1217 |diag| {1218 diag.span_suggestion_short(1219 refdefspan.shrink_to_hi(),1220 "for an intra-doc link, add `[]` between the label and the colon",1221 "[]",1222 Applicability::MaybeIncorrect,1223 );1224 diag.help("link definitions are not shown in rendered documentation");1225 }1226 );1227 refdefrange.start - range.start1228 } else {1229 let mut start = next_range.start;1230 if start > 0 && doc.as_bytes().get(start - 1) == Some(&b'\\') {1231 // backslashes aren't in the event stream...1232 start -= 1;1233 }12341235 start.saturating_sub(range.start)1236 }1237 } else {1238 01239 };1240 containers.push(Container::List(indent));1241 }1242 ticks_unbalanced = false;1243 paragraph_range = range;1244 if is_first_paragraph {1245 headers.first_paragraph_md_len = doc[paragraph_range.clone()].chars().count();1246 }1247 },1248 End(TagEnd::Heading(_) | TagEnd::Paragraph | TagEnd::Item) => {1249 if is_first_paragraph {1250 is_first_paragraph = false;1251 }1252 if let End(TagEnd::Heading(_)) = event {1253 in_heading = false;1254 }1255 if let End(TagEnd::Item) = event {1256 containers.pop();1257 }1258 if check_doc_markdown {1259 if ticks_unbalanced && let Some(span) = fragments.span(cx, paragraph_range.clone())1260 .or_else(|| span_of_fragments(fragments.fragments)) {1261 span_lint_and_help(1262 cx,1263 DOC_MARKDOWN,1264 span,1265 "backticks are unbalanced",1266 None,1267 "a backtick may be missing a pair",1268 );1269 text_to_check.clear();1270 } else {1271 for (text, range, assoc_code_level) in text_to_check.drain(..) {1272 markdown::check(1273 cx, valid_idents, &text, &fragments, range, assoc_code_level, blockquote_level1274 );1275 }1276 }1277 }1278 },1279 Start(FootnoteDefinition(..)) => in_footnote_definition = true,1280 End(TagEnd::FootnoteDefinition) => in_footnote_definition = false,1281 Start(_) | End(_) // We don't care about other tags1282 | TaskListMarker(_) | Rule => (),1283 SoftBreak | HardBreak => {1284 if is_first_paragraph {1285 headers.first_paragraph_text_len += 1;1286 }1287 if !containers.is_empty()1288 && !in_footnote_definition1289 // Tabs aren't handled correctly vvvv1290 && !doc[range.clone()].contains('\t')1291 && let Some((next_event, next_range)) = events.peek()1292 && !matches!(next_event, End(_))1293 {1294 lazy_continuation::check(1295 cx,1296 doc,1297 range.end..next_range.start,1298 &fragments,1299 &containers[..],1300 );1301 }130213031304 if event == HardBreak1305 && !doc[range.clone()].trim().starts_with('\\')1306 && let Some(span) = fragments.span(cx, range.clone())1307 && !span.from_expansion()1308 {1309 collected_breaks.push(span);1310 }1311 },1312 Code(code) | InlineMath(code) | DisplayMath(code) => {1313 if is_first_paragraph {1314 headers.first_paragraph_text_len += code.chars().count();1315 }1316 }1317 Text(text) => {1318 if is_first_paragraph {1319 headers.first_paragraph_text_len += text.chars().count();1320 }1321 paragraph_range.end = range.end;1322 let range_ = range.clone();1323 ticks_unbalanced |= text.contains('`')1324 && code.is_none()1325 && doc[range.clone()].bytes().enumerate().any(|(i, c)| {1326 // scan the markdown source code bytes for backquotes that aren't preceded by backslashes1327 // - use bytes, instead of chars, to avoid utf8 decoding overhead (special chars are ascii)1328 // - relevant backquotes are within doc[range], but backslashes are not, because they're not1329 // actually part of the rendered text (pulldown-cmark doesn't emit any events for escapes)1330 // - if `range_.start + i == 0`, then `range_.start + i - 1 == -1`, and since we're working in1331 // usize, that would underflow and maybe panic1332 c == b'`' && (range_.start + i == 0 || doc.as_bytes().get(range_.start + i - 1) != Some(&b'\\'))1333 });1334 if Some(&text) == in_link.as_ref() || ticks_unbalanced {1335 // Probably a link of the form `<http://example.com>`1336 // Which are represented as a link to "http://example.com" with1337 // text "http://example.com" by pulldown-cmark1338 continue;1339 }1340 let trimmed_text = text.trim();1341 headers.safety |= in_heading && trimmed_text == "Safety";1342 headers.safety |= in_heading && trimmed_text == "SAFETY";1343 headers.safety |= in_heading && trimmed_text == "Implementation safety";1344 headers.safety |= in_heading && trimmed_text == "Implementation Safety";1345 headers.errors |= in_heading && trimmed_text == "Errors";1346 headers.panics |= in_heading && trimmed_text == "Panics";13471348 if let Some(tags) = code {1349 if tags.rust && !tags.compile_fail && !tags.ignore {1350 needless_doctest_main::check(cx, &text, range.start, fragments);13511352 if !tags.no_run && !tags.test_harness {1353 test_attr_in_doctest::check(cx, &text, range.start, fragments);1354 }1355 }1356 } else {1357 if in_link.is_some() {1358 link_with_quotes::check(cx, trimmed_text, range.clone(), fragments);1359 }1360 if let Some(link) = in_link.as_ref()1361 && let Ok(url) = Url::parse(link)1362 && (url.scheme() == "https" || url.scheme() == "http")1363 {1364 // Don't check the text associated with external URLs1365 continue;1366 }1367 if check_doc_markdown {1368 text_to_check.push((text, range.clone(), code_level));1369 }1370 doc_suspicious_footnotes::check(cx, doc, range, &fragments, attrs);1371 }1372 }1373 FootnoteReference(_) => {}1374 }1375 }13761377 doc_comment_double_space_linebreaks::check(cx, &collected_breaks);13781379 headers1380}13811382fn looks_like_refdef(doc: &str, range: Range<usize>) -> Option<Range<usize>> {1383 if range.end < range.start {1384 return None;1385 }13861387 let offset = range.start;1388 let mut iterator = doc.as_bytes()[range].iter().copied().enumerate();1389 let mut start = None;1390 while let Some((i, byte)) = iterator.next() {1391 match byte {1392 b'\\' => {1393 iterator.next();1394 },1395 b'[' => {1396 start = Some(i + offset);1397 },1398 b']' if let Some(start) = start1399 && doc.as_bytes().get(i + offset + 1) == Some(&b':') =>1400 {1401 return Some(start..i + offset + 1);1402 },1403 _ => {},1404 }1405 }1406 None1407}