1#![allow(clippy::lint_without_lint_pass)]23use clippy_config::Conf;4use clippy_utils::attrs::is_doc_hidden;5use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_then};6use clippy_utils::{is_entrypoint_fn, is_trait_impl_item};7use rustc_data_structures::fx::FxHashSet;8use rustc_errors::Applicability;9use rustc_hir::{Attribute, ImplItemKind, ItemKind, Node, Safety, TraitItemKind};10use rustc_lint::{EarlyContext, EarlyLintPass, LateContext, LateLintPass, LintContext};11use rustc_resolve::rustdoc::pulldown_cmark::Event::{12 Code, DisplayMath, End, FootnoteReference, HardBreak, Html, InlineHtml, InlineMath, Rule, SoftBreak, Start,13 TaskListMarker, Text,14};15use rustc_resolve::rustdoc::pulldown_cmark::Tag::{16 BlockQuote, CodeBlock, FootnoteDefinition, Heading, Item, Link, Paragraph,17};18use rustc_resolve::rustdoc::pulldown_cmark::{BrokenLink, CodeBlockKind, CowStr, Options, TagEnd};19use rustc_resolve::rustdoc::{20 DocFragment, add_doc_fragment, attrs_to_doc_fragments, main_body_opts, pulldown_cmark,21 source_span_for_markdown_range, span_of_fragments,22};23use rustc_session::impl_lint_pass;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 comment linebreaks that use double spaces to separate lines, instead of back-slash (`\`).73 ///74 /// ### Why is this bad?75 /// Double spaces, when used as doc comment linebreaks, can be difficult to see, and may76 /// accidentally be removed during automatic formatting or manual refactoring. The use of a back-slash (`\`)77 /// is clearer in this regard.78 ///79 /// ### Example80 /// The two replacement dots in this example represent a double space.81 /// ```no_run82 /// /// This command takes two numbers as inputs and··83 /// /// adds them together, and then returns the result.84 /// fn add(l: i32, r: i32) -> i32 {85 /// l + r86 /// }87 /// ```88 ///89 /// Use instead:90 /// ```no_run91 /// /// This command takes two numbers as inputs and\92 /// /// adds them together, and then returns the result.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 linebreak 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 main() {}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 /// ```350 /// Use instead:351 /// ```no_run352 /// /// Returns a random number.353 /// ///354 /// /// It was chosen by a fair dice roll.355 /// ```356 ///357 /// ### Terminal punctuation marks358 /// This lint treats these characters as end markers: '.', '?', '!', '…' and ':'.359 ///360 /// The colon is not exactly a terminal punctuation mark, but this is required for paragraphs that361 /// introduce a table or a list for example.362 #[clippy::version = "1.93.0"]363 pub DOC_PARAGRAPHS_MISSING_PUNCTUATION,364 restriction,365 "missing terminal punctuation in doc comments"366}367368declare_clippy_lint! {369 /// ### What it does370 /// Detects syntax that looks like a footnote reference.371 ///372 /// Rustdoc footnotes are compatible with GitHub-Flavored Markdown (GFM).373 /// GFM does not parse a footnote reference unless its definition also374 /// exists. This lint checks for footnote references with missing375 /// definitions, unless it thinks you're writing a regex.376 ///377 /// ### Why is this bad?378 /// This probably means that a footnote was meant to exist,379 /// but was not written.380 ///381 /// ### Example382 /// ```no_run383 /// /// This is not a footnote[^1], because no definition exists.384 /// fn my_fn() {}385 /// ```386 /// Use instead:387 /// ```no_run388 /// /// This is a footnote[^1].389 /// ///390 /// /// [^1]: defined here391 /// fn my_fn() {}392 /// ```393 #[clippy::version = "1.89.0"]394 pub DOC_SUSPICIOUS_FOOTNOTES,395 suspicious,396 "looks like a link or footnote ref, but with no definition"397}398399declare_clippy_lint! {400 /// ### What it does401 /// Detects documentation that is empty.402 /// ### Why is this bad?403 /// Empty docs clutter code without adding value, reducing readability and maintainability.404 /// ### Example405 /// ```no_run406 /// ///407 /// fn returns_true() -> bool {408 /// true409 /// }410 /// ```411 /// Use instead:412 /// ```no_run413 /// fn returns_true() -> bool {414 /// true415 /// }416 /// ```417 #[clippy::version = "1.78.0"]418 pub EMPTY_DOCS,419 suspicious,420 "docstrings exist but documentation is empty"421}422423declare_clippy_lint! {424 /// ### What it does425 /// Checks the doc comments of publicly visible functions that426 /// return a `Result` type and warns if there is no `# Errors` section.427 ///428 /// ### Why is this bad?429 /// Documenting the type of errors that can be returned from a430 /// function can help callers write code to handle the errors appropriately.431 ///432 /// ### Examples433 /// Since the following function returns a `Result` it has an `# Errors` section in434 /// its doc comment:435 ///436 /// ```no_run437 ///# use std::io;438 /// /// # Errors439 /// ///440 /// /// Will return `Err` if `filename` does not exist or the user does not have441 /// /// permission to read it.442 /// pub fn read(filename: String) -> io::Result<String> {443 /// unimplemented!();444 /// }445 /// ```446 #[clippy::version = "1.41.0"]447 pub MISSING_ERRORS_DOC,448 pedantic,449 "`pub fn` returns `Result` without `# Errors` in doc comment"450}451452declare_clippy_lint! {453 /// ### What it does454 /// Checks the doc comments of publicly visible functions that455 /// may panic and warns if there is no `# Panics` section.456 ///457 /// ### Why is this bad?458 /// Documenting the scenarios in which panicking occurs459 /// can help callers who do not want to panic to avoid those situations.460 ///461 /// ### Examples462 /// Since the following function may panic it has a `# Panics` section in463 /// its doc comment:464 ///465 /// ```no_run466 /// /// # Panics467 /// ///468 /// /// Will panic if y is 0469 /// pub fn divide_by(x: i32, y: i32) -> i32 {470 /// if y == 0 {471 /// panic!("Cannot divide by 0")472 /// } else {473 /// x / y474 /// }475 /// }476 /// ```477 ///478 /// Individual panics within a function can be ignored with `#[expect]` or479 /// `#[allow]`:480 ///481 /// ```no_run482 /// # use std::num::NonZeroUsize;483 /// pub fn will_not_panic(x: usize) {484 /// #[expect(clippy::missing_panics_doc, reason = "infallible")]485 /// let y = NonZeroUsize::new(1).unwrap();486 ///487 /// // If any panics are added in the future the lint will still catch them488 /// }489 /// ```490 #[clippy::version = "1.51.0"]491 pub MISSING_PANICS_DOC,492 pedantic,493 "`pub fn` may panic without `# Panics` in doc comment"494}495496declare_clippy_lint! {497 /// ### What it does498 /// Checks for the doc comments of publicly visible499 /// unsafe functions and warns if there is no `# Safety` section.500 ///501 /// ### Why is this bad?502 /// Unsafe functions should document their safety503 /// preconditions, so that users can be sure they are using them safely.504 ///505 /// ### Examples506 /// ```no_run507 ///# type Universe = ();508 /// /// This function should really be documented509 /// pub unsafe fn start_apocalypse(u: &mut Universe) {510 /// unimplemented!();511 /// }512 /// ```513 ///514 /// At least write a line about safety:515 ///516 /// ```no_run517 ///# type Universe = ();518 /// /// # Safety519 /// ///520 /// /// This function should not be called before the horsemen are ready.521 /// pub unsafe fn start_apocalypse(u: &mut Universe) {522 /// unimplemented!();523 /// }524 /// ```525 #[clippy::version = "1.39.0"]526 pub MISSING_SAFETY_DOC,527 style,528 "`pub unsafe fn` without `# Safety` docs"529}530531declare_clippy_lint! {532 /// ### What it does533 /// Checks for `fn main() { .. }` in doctests534 ///535 /// ### Why is this bad?536 /// The test can be shorter (and likely more readable)537 /// if the `fn main()` is left implicit.538 ///539 /// ### Examples540 /// ```no_run541 /// /// An example of a doctest with a `main()` function542 /// ///543 /// /// # Examples544 /// ///545 /// /// ```546 /// /// fn main() {547 /// /// // this needs not be in an `fn`548 /// /// }549 /// /// ```550 /// fn needless_main() {551 /// unimplemented!();552 /// }553 /// ```554 #[clippy::version = "1.40.0"]555 pub NEEDLESS_DOCTEST_MAIN,556 style,557 "presence of `fn main() {` in code examples"558}559560declare_clippy_lint! {561 /// ### What it does562 /// Detects the use of outer doc comments (`///`, `/**`) followed by a bang (`!`): `///!`563 ///564 /// ### Why is this bad?565 /// Triple-slash comments (known as "outer doc comments") apply to items that follow it.566 /// An outer doc comment followed by a bang (i.e. `///!`) has no specific meaning.567 ///568 /// The user most likely meant to write an inner doc comment (`//!`, `/*!`), which569 /// applies to the parent item (i.e. the item that the comment is contained in,570 /// usually a module or crate).571 ///572 /// ### Known problems573 /// Inner doc comments can only appear before items, so there are certain cases where the suggestion574 /// made by this lint is not valid code. For example:575 /// ```rust576 /// fn foo() {}577 /// ///!578 /// fn bar() {}579 /// ```580 /// This lint detects the doc comment and suggests changing it to `//!`, but an inner doc comment581 /// is not valid at that position.582 ///583 /// ### Example584 /// In this example, the doc comment is attached to the *function*, rather than the *module*.585 /// ```no_run586 /// pub mod util {587 /// ///! This module contains utility functions.588 ///589 /// pub fn dummy() {}590 /// }591 /// ```592 ///593 /// Use instead:594 /// ```no_run595 /// pub mod util {596 /// //! This module contains utility functions.597 ///598 /// pub fn dummy() {}599 /// }600 /// ```601 #[clippy::version = "1.70.0"]602 pub SUSPICIOUS_DOC_COMMENTS,603 suspicious,604 "suspicious usage of (outer) doc comments"605}606607declare_clippy_lint! {608 /// ### What it does609 /// Checks for `#[test]` in doctests unless they are marked with610 /// either `ignore`, `no_run` or `compile_fail`.611 ///612 /// ### Why is this bad?613 /// Code in examples marked as `#[test]` will somewhat614 /// surprisingly not be run by `cargo test`. If you really want615 /// to show how to test stuff in an example, mark it `no_run` to616 /// make the intent clear.617 ///618 /// ### Examples619 /// ```no_run620 /// /// An example of a doctest with a `main()` function621 /// ///622 /// /// # Examples623 /// ///624 /// /// ```625 /// /// #[test]626 /// /// fn equality_works() {627 /// /// assert_eq!(1_u8, 1);628 /// /// }629 /// /// ```630 /// fn test_attr_in_doctest() {631 /// unimplemented!();632 /// }633 /// ```634 #[clippy::version = "1.76.0"]635 pub TEST_ATTR_IN_DOCTEST,636 suspicious,637 "presence of `#[test]` in code examples"638}639640declare_clippy_lint! {641 /// ### What it does642 /// Checks if the first paragraph in the documentation of items listed in the module page is too long.643 ///644 /// ### Why is this bad?645 /// Documentation will show the first paragraph of the docstring in the summary page of a646 /// module. Having a nice, short summary in the first paragraph is part of writing good docs.647 ///648 /// ### Example649 /// ```no_run650 /// /// A very short summary.651 /// /// A much longer explanation that goes into a lot more detail about652 /// /// how the thing works, possibly with doclinks and so one,653 /// /// and probably spanning a many rows.654 /// struct Foo {}655 /// ```656 /// Use instead:657 /// ```no_run658 /// /// A very short summary.659 /// ///660 /// /// A much longer explanation that goes into a lot more detail about661 /// /// how the thing works, possibly with doclinks and so one,662 /// /// and probably spanning a many rows.663 /// struct Foo {}664 /// ```665 #[clippy::version = "1.82.0"]666 pub TOO_LONG_FIRST_DOC_PARAGRAPH,667 nursery,668 "ensure the first documentation paragraph is short"669}670671declare_clippy_lint! {672 /// ### What it does673 /// Checks for the doc comments of publicly visible674 /// safe functions and traits and warns if there is a `# Safety` section.675 ///676 /// ### Why restrict this?677 /// Safe functions and traits are safe to implement and therefore do not678 /// need to describe safety preconditions that users are required to uphold.679 ///680 /// ### Examples681 /// ```no_run682 ///# type Universe = ();683 /// /// # Safety684 /// ///685 /// /// This function should not be called before the horsemen are ready.686 /// pub fn start_apocalypse_but_safely(u: &mut Universe) {687 /// unimplemented!();688 /// }689 /// ```690 ///691 /// The function is safe, so there shouldn't be any preconditions692 /// that have to be explained for safety reasons.693 ///694 /// ```no_run695 ///# type Universe = ();696 /// /// This function should really be documented697 /// pub fn start_apocalypse(u: &mut Universe) {698 /// unimplemented!();699 /// }700 /// ```701 #[clippy::version = "1.67.0"]702 pub UNNECESSARY_SAFETY_DOC,703 restriction,704 "`pub fn` or `pub trait` with `# Safety` docs"705}706707impl_lint_pass!(Documentation => [708 DOC_BROKEN_LINK,709 DOC_COMMENT_DOUBLE_SPACE_LINEBREAKS,710 DOC_INCLUDE_WITHOUT_CFG,711 DOC_LAZY_CONTINUATION,712 DOC_LINK_CODE,713 DOC_LINK_WITH_QUOTES,714 DOC_MARKDOWN,715 DOC_NESTED_REFDEFS,716 DOC_OVERINDENTED_LIST_ITEMS,717 DOC_PARAGRAPHS_MISSING_PUNCTUATION,718 DOC_SUSPICIOUS_FOOTNOTES,719 EMPTY_DOCS,720 MISSING_ERRORS_DOC,721 MISSING_PANICS_DOC,722 MISSING_SAFETY_DOC,723 NEEDLESS_DOCTEST_MAIN,724 SUSPICIOUS_DOC_COMMENTS,725 TEST_ATTR_IN_DOCTEST,726 TOO_LONG_FIRST_DOC_PARAGRAPH,727 UNNECESSARY_SAFETY_DOC,728]);729730pub struct Documentation {731 valid_idents: FxHashSet<String>,732 check_private_items: bool,733}734735impl Documentation {736 pub fn new(conf: &'static Conf) -> Self {737 Self {738 valid_idents: conf.doc_valid_idents.iter().cloned().collect(),739 check_private_items: conf.check_private_items,740 }741 }742}743744impl EarlyLintPass for Documentation {745 fn check_attributes(&mut self, cx: &EarlyContext<'_>, attrs: &[rustc_ast::Attribute]) {746 include_in_doc_without_cfg::check(cx, attrs);747 }748}749750impl<'tcx> LateLintPass<'tcx> for Documentation {751 fn check_attributes(&mut self, cx: &LateContext<'tcx>, attrs: &'tcx [Attribute]) {752 let Some(headers) = check_attrs(cx, &self.valid_idents, attrs) else {753 return;754 };755756 match cx.tcx.hir_node(cx.last_node_with_lint_attrs) {757 Node::Item(item) => {758 too_long_first_doc_paragraph::check(759 cx,760 item,761 attrs,762 headers.first_paragraph_len,763 self.check_private_items,764 );765 match item.kind {766 ItemKind::Fn { sig, body, .. }767 if !(is_entrypoint_fn(cx, item.owner_id.to_def_id())768 || item.span.in_external_macro(cx.tcx.sess.source_map())) =>769 {770 missing_headers::check(cx, item.owner_id, sig, headers, Some(body), self.check_private_items);771 },772 ItemKind::Trait { safety, .. } => match (headers.safety, safety) {773 (false, Safety::Unsafe) => span_lint(774 cx,775 MISSING_SAFETY_DOC,776 cx.tcx.def_span(item.owner_id),777 "docs for unsafe trait missing `# Safety` section",778 ),779 (true, Safety::Safe) => span_lint(780 cx,781 UNNECESSARY_SAFETY_DOC,782 cx.tcx.def_span(item.owner_id),783 "docs for safe trait have unnecessary `# Safety` section",784 ),785 _ => (),786 },787 _ => (),788 }789 },790 Node::TraitItem(trait_item) => {791 if let TraitItemKind::Fn(sig, ..) = trait_item.kind792 && !trait_item.span.in_external_macro(cx.tcx.sess.source_map())793 {794 missing_headers::check(cx, trait_item.owner_id, sig, headers, None, self.check_private_items);795 }796 },797 Node::ImplItem(impl_item) => {798 if let ImplItemKind::Fn(sig, body_id) = impl_item.kind799 && !impl_item.span.in_external_macro(cx.tcx.sess.source_map())800 && !is_trait_impl_item(cx, impl_item.hir_id())801 {802 missing_headers::check(803 cx,804 impl_item.owner_id,805 sig,806 headers,807 Some(body_id),808 self.check_private_items,809 );810 }811 },812 _ => {},813 }814 }815}816817#[derive(Copy, Clone)]818struct Fragments<'a> {819 doc: &'a str,820 fragments: &'a [DocFragment],821}822823impl Fragments<'_> {824 /// get the span for the markdown range. Note that this function is not cheap, use it with825 /// caution.826 #[must_use]827 fn span(self, cx: &LateContext<'_>, range: Range<usize>) -> Option<Span> {828 source_span_for_markdown_range(cx.tcx, self.doc, &range, self.fragments).map(|(sp, _)| sp)829 }830}831832#[derive(Copy, Clone, Default)]833struct DocHeaders {834 safety: bool,835 errors: bool,836 panics: bool,837 first_paragraph_len: usize,838}839840/// Does some pre-processing on raw, desugared `#[doc]` attributes such as parsing them and841/// then delegates to `check_doc`.842/// Some lints are already checked here if they can work with attributes directly and don't need843/// to work with markdown.844/// Others are checked elsewhere, e.g. in `check_doc` if they need access to markdown, or845/// back in the various late lint pass methods if they need the final doc headers, like "Safety" or846/// "Panics" sections.847fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[Attribute]) -> Option<DocHeaders> {848 // We don't want the parser to choke on intra doc links. Since we don't849 // actually care about rendering them, just pretend that all broken links850 // point to a fake address.851 #[expect(clippy::unnecessary_wraps)] // we're following a type signature852 fn fake_broken_link_callback<'a>(_: BrokenLink<'_>) -> Option<(CowStr<'a>, CowStr<'a>)> {853 Some(("fake".into(), "fake".into()))854 }855856 if suspicious_doc_comments::check(cx, attrs) || is_doc_hidden(attrs) {857 return None;858 }859860 let (fragments, _) = attrs_to_doc_fragments(861 attrs.iter().filter_map(|attr| {862 if attr.doc_str_and_fragment_kind().is_none() || attr.span().in_external_macro(cx.sess().source_map()) {863 None864 } else {865 Some((attr, None))866 }867 }),868 true,869 );870871 let mut doc = String::with_capacity(fragments.iter().map(|frag| frag.doc.as_str().len() + 1).sum());872873 for fragment in &fragments {874 add_doc_fragment(&mut doc, fragment);875 }876 doc.pop();877878 if doc.trim().is_empty() {879 if let Some(span) = span_of_fragments(&fragments) {880 span_lint_and_help(881 cx,882 EMPTY_DOCS,883 span,884 "empty doc comment",885 None,886 "consider removing or filling it",887 );888 }889 return Some(DocHeaders::default());890 }891892 check_for_code_clusters(893 cx,894 pulldown_cmark::Parser::new_with_broken_link_callback(895 &doc,896 main_body_opts() - Options::ENABLE_SMART_PUNCTUATION,897 Some(&mut fake_broken_link_callback),898 )899 .into_offset_iter(),900 &doc,901 Fragments {902 doc: &doc,903 fragments: &fragments,904 },905 );906907 doc_paragraphs_missing_punctuation::check(908 cx,909 &doc,910 Fragments {911 doc: &doc,912 fragments: &fragments,913 },914 );915916 // NOTE: check_doc uses it own cb function,917 // to avoid causing duplicated diagnostics for the broken link checker.918 let mut full_fake_broken_link_callback = |bl: BrokenLink<'_>| -> Option<(CowStr<'_>, CowStr<'_>)> {919 broken_link::check(cx, &bl, &doc, &fragments);920 Some(("fake".into(), "fake".into()))921 };922923 // disable smart punctuation to pick up ['link'] more easily924 let opts = main_body_opts() - Options::ENABLE_SMART_PUNCTUATION;925 let parser =926 pulldown_cmark::Parser::new_with_broken_link_callback(&doc, opts, Some(&mut full_fake_broken_link_callback));927928 Some(check_doc(929 cx,930 valid_idents,931 parser.into_offset_iter(),932 &doc,933 Fragments {934 doc: &doc,935 fragments: &fragments,936 },937 attrs,938 ))939}940941enum Container {942 Blockquote,943 List(usize),944}945946/// Scan the documentation for code links that are back-to-back with code spans.947///948/// This is done separately from the rest of the docs, because that makes it easier to produce949/// the correct messages.950fn check_for_code_clusters<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize>)>>(951 cx: &LateContext<'_>,952 events: Events,953 doc: &str,954 fragments: Fragments<'_>,955) {956 let mut events = events.peekable();957 let mut code_starts_at = None;958 let mut code_ends_at = None;959 let mut code_includes_link = false;960 while let Some((event, range)) = events.next() {961 match event {962 Start(Link { .. }) if matches!(events.peek(), Some((Code(_), _range))) => {963 if code_starts_at.is_some() {964 code_ends_at = Some(range.end);965 } else {966 code_starts_at = Some(range.start);967 }968 code_includes_link = true;969 // skip the nested "code", because we're already handling it here970 let _ = events.next();971 },972 Code(_) => {973 if code_starts_at.is_some() {974 code_ends_at = Some(range.end);975 } else {976 code_starts_at = Some(range.start);977 }978 },979 End(TagEnd::Link) => {},980 _ => {981 if let Some(start) = code_starts_at982 && let Some(end) = code_ends_at983 && code_includes_link984 && let Some(span) = fragments.span(cx, start..end)985 {986 span_lint_and_then(cx, DOC_LINK_CODE, span, "code link adjacent to code text", |diag| {987 let sugg = format!("<code>{}</code>", doc[start..end].replace('`', ""));988 diag.span_suggestion_verbose(989 span,990 "wrap the entire group in `<code>` tags",991 sugg,992 Applicability::MaybeIncorrect,993 );994 diag.help("separate code snippets will be shown with a gap");995 });996 }997 code_includes_link = false;998 code_starts_at = None;999 code_ends_at = None;1000 },1001 }1002 }1003}10041005#[derive(Clone, Copy)]1006#[expect(clippy::struct_excessive_bools)]1007struct CodeTags {1008 no_run: bool,1009 ignore: bool,1010 compile_fail: bool,1011 test_harness: bool,10121013 rust: bool,1014}10151016impl Default for CodeTags {1017 fn default() -> Self {1018 Self {1019 no_run: false,1020 ignore: false,1021 compile_fail: false,1022 test_harness: false,10231024 rust: true,1025 }1026 }1027}10281029impl CodeTags {1030 /// Based on <https://github.com/rust-lang/rust/blob/1.90.0/src/librustdoc/html/markdown.rs#L1169>1031 fn parse(lang: &str) -> Self {1032 let mut tags = Self::default();10331034 let mut seen_rust_tags = false;1035 let mut seen_other_tags = false;1036 for item in lang.split([',', ' ', '\t']) {1037 match item.trim() {1038 "" => {},1039 "rust" => {1040 tags.rust = true;1041 seen_rust_tags = true;1042 },1043 "ignore" => {1044 tags.ignore = true;1045 seen_rust_tags = !seen_other_tags;1046 },1047 "no_run" => {1048 tags.no_run = true;1049 seen_rust_tags = !seen_other_tags;1050 },1051 "should_panic" => seen_rust_tags = !seen_other_tags,1052 "compile_fail" => {1053 tags.compile_fail = true;1054 seen_rust_tags = !seen_other_tags || seen_rust_tags;1055 },1056 "test_harness" => {1057 tags.test_harness = true;1058 seen_rust_tags = !seen_other_tags || seen_rust_tags;1059 },1060 "standalone_crate" => {1061 seen_rust_tags = !seen_other_tags || seen_rust_tags;1062 },1063 _ if item.starts_with("ignore-") => seen_rust_tags = true,1064 _ if item.starts_with("edition") => {},1065 _ => seen_other_tags = true,1066 }1067 }10681069 tags.rust &= seen_rust_tags || !seen_other_tags;10701071 tags1072 }1073}10741075/// Checks parsed documentation.1076/// This walks the "events" (think sections of markdown) produced by `pulldown_cmark`,1077/// so lints here will generally access that information.1078/// Returns documentation headers -- whether a "Safety", "Errors", "Panic" section was found1079#[expect(clippy::too_many_lines, reason = "big match statement")]1080fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize>)>>(1081 cx: &LateContext<'_>,1082 valid_idents: &FxHashSet<String>,1083 events: Events,1084 doc: &str,1085 fragments: Fragments<'_>,1086 attrs: &[Attribute],1087) -> DocHeaders {1088 // true if a safety header was found1089 let mut headers = DocHeaders::default();1090 let mut code = None;1091 let mut in_link = None;1092 let mut in_heading = false;1093 let mut in_footnote_definition = false;1094 let mut ticks_unbalanced = false;1095 let mut text_to_check: Vec<(CowStr<'_>, Range<usize>, isize)> = Vec::new();1096 let mut paragraph_range = 0..0;1097 let mut code_level = 0;1098 let mut blockquote_level = 0;1099 let mut collected_breaks: Vec<Span> = Vec::new();1100 let mut is_first_paragraph = true;11011102 let mut containers = Vec::new();11031104 let mut events = events.peekable();11051106 while let Some((event, range)) = events.next() {1107 match event {1108 Html(tag) | InlineHtml(tag) => {1109 if tag.starts_with("<code") {1110 code_level += 1;1111 } else if tag.starts_with("</code") {1112 code_level -= 1;1113 } else if tag.starts_with("<blockquote") || tag.starts_with("<q") {1114 blockquote_level += 1;1115 } else if tag.starts_with("</blockquote") || tag.starts_with("</q") {1116 blockquote_level -= 1;1117 }1118 },1119 Start(BlockQuote(_)) => {1120 blockquote_level += 1;1121 containers.push(Container::Blockquote);1122 if let Some((next_event, next_range)) = events.peek() {1123 let next_start = match next_event {1124 End(TagEnd::BlockQuote) => next_range.end,1125 _ => next_range.start,1126 };1127 if let Some(refdefrange) = looks_like_refdef(doc, range.start..next_start) &&1128 let Some(refdefspan) = fragments.span(cx, refdefrange.clone())1129 {1130 span_lint_and_then(1131 cx,1132 DOC_NESTED_REFDEFS,1133 refdefspan,1134 "link reference defined in quote",1135 |diag| {1136 diag.span_suggestion_short(1137 refdefspan.shrink_to_hi(),1138 "for an intra-doc link, add `[]` between the label and the colon",1139 "[]",1140 Applicability::MaybeIncorrect,1141 );1142 diag.help("link definitions are not shown in rendered documentation");1143 }1144 );1145 }1146 }1147 },1148 End(TagEnd::BlockQuote) => {1149 blockquote_level -= 1;1150 containers.pop();1151 },1152 Start(CodeBlock(ref kind)) => {1153 code = Some(match kind {1154 CodeBlockKind::Indented => CodeTags::default(),1155 CodeBlockKind::Fenced(lang) => CodeTags::parse(lang),1156 });1157 },1158 End(TagEnd::CodeBlock) => code = None,1159 Start(Link { dest_url, .. }) => in_link = Some(dest_url),1160 End(TagEnd::Link) => in_link = None,1161 Start(Heading { .. } | Paragraph | Item) => {1162 if let Start(Heading { .. }) = event {1163 in_heading = true;1164 }1165 if let Start(Item) = event {1166 let indent = if let Some((next_event, next_range)) = events.peek() {1167 let next_start = match next_event {1168 End(TagEnd::Item) => next_range.end,1169 _ => next_range.start,1170 };1171 if let Some(refdefrange) = looks_like_refdef(doc, range.start..next_start) &&1172 let Some(refdefspan) = fragments.span(cx, refdefrange.clone())1173 {1174 span_lint_and_then(1175 cx,1176 DOC_NESTED_REFDEFS,1177 refdefspan,1178 "link reference defined in list item",1179 |diag| {1180 diag.span_suggestion_short(1181 refdefspan.shrink_to_hi(),1182 "for an intra-doc link, add `[]` between the label and the colon",1183 "[]",1184 Applicability::MaybeIncorrect,1185 );1186 diag.help("link definitions are not shown in rendered documentation");1187 }1188 );1189 refdefrange.start - range.start1190 } else {1191 let mut start = next_range.start;1192 if start > 0 && doc.as_bytes().get(start - 1) == Some(&b'\\') {1193 // backslashes aren't in the event stream...1194 start -= 1;1195 }11961197 start.saturating_sub(range.start)1198 }1199 } else {1200 01201 };1202 containers.push(Container::List(indent));1203 }1204 ticks_unbalanced = false;1205 paragraph_range = range;1206 if is_first_paragraph {1207 headers.first_paragraph_len = doc[paragraph_range.clone()].chars().count();1208 is_first_paragraph = false;1209 }1210 },1211 End(TagEnd::Heading(_) | TagEnd::Paragraph | TagEnd::Item) => {1212 if let End(TagEnd::Heading(_)) = event {1213 in_heading = false;1214 }1215 if let End(TagEnd::Item) = event {1216 containers.pop();1217 }1218 if ticks_unbalanced && let Some(span) = fragments.span(cx, paragraph_range.clone()) {1219 span_lint_and_help(1220 cx,1221 DOC_MARKDOWN,1222 span,1223 "backticks are unbalanced",1224 None,1225 "a backtick may be missing a pair",1226 );1227 text_to_check.clear();1228 } else {1229 for (text, range, assoc_code_level) in text_to_check.drain(..) {1230 markdown::check(cx, valid_idents, &text, &fragments, range, assoc_code_level, blockquote_level);1231 }1232 }1233 },1234 Start(FootnoteDefinition(..)) => in_footnote_definition = true,1235 End(TagEnd::FootnoteDefinition) => in_footnote_definition = false,1236 Start(_) | End(_) // We don't care about other tags1237 | TaskListMarker(_) | Code(_) | Rule | InlineMath(..) | DisplayMath(..) => (),1238 SoftBreak | HardBreak => {1239 if !containers.is_empty()1240 && !in_footnote_definition1241 // Tabs aren't handled correctly vvvv1242 && !doc[range.clone()].contains('\t')1243 && let Some((next_event, next_range)) = events.peek()1244 && !matches!(next_event, End(_))1245 {1246 lazy_continuation::check(1247 cx,1248 doc,1249 range.end..next_range.start,1250 &fragments,1251 &containers[..],1252 );1253 }125412551256 if event == HardBreak1257 && !doc[range.clone()].trim().starts_with('\\')1258 && let Some(span) = fragments.span(cx, range.clone())1259 && !span.from_expansion()1260 {1261 collected_breaks.push(span);1262 }1263 },1264 Text(text) => {1265 paragraph_range.end = range.end;1266 let range_ = range.clone();1267 ticks_unbalanced |= text.contains('`')1268 && code.is_none()1269 && doc[range.clone()].bytes().enumerate().any(|(i, c)| {1270 // scan the markdown source code bytes for backquotes that aren't preceded by backslashes1271 // - use bytes, instead of chars, to avoid utf8 decoding overhead (special chars are ascii)1272 // - relevant backquotes are within doc[range], but backslashes are not, because they're not1273 // actually part of the rendered text (pulldown-cmark doesn't emit any events for escapes)1274 // - if `range_.start + i == 0`, then `range_.start + i - 1 == -1`, and since we're working in1275 // usize, that would underflow and maybe panic1276 c == b'`' && (range_.start + i == 0 || doc.as_bytes().get(range_.start + i - 1) != Some(&b'\\'))1277 });1278 if Some(&text) == in_link.as_ref() || ticks_unbalanced {1279 // Probably a link of the form `<http://example.com>`1280 // Which are represented as a link to "http://example.com" with1281 // text "http://example.com" by pulldown-cmark1282 continue;1283 }1284 let trimmed_text = text.trim();1285 headers.safety |= in_heading && trimmed_text == "Safety";1286 headers.safety |= in_heading && trimmed_text == "SAFETY";1287 headers.safety |= in_heading && trimmed_text == "Implementation safety";1288 headers.safety |= in_heading && trimmed_text == "Implementation Safety";1289 headers.errors |= in_heading && trimmed_text == "Errors";1290 headers.panics |= in_heading && trimmed_text == "Panics";12911292 if let Some(tags) = code {1293 if tags.rust && !tags.compile_fail && !tags.ignore {1294 needless_doctest_main::check(cx, &text, range.start, fragments);12951296 if !tags.no_run && !tags.test_harness {1297 test_attr_in_doctest::check(cx, &text, range.start, fragments);1298 }1299 }1300 } else {1301 if in_link.is_some() {1302 link_with_quotes::check(cx, trimmed_text, range.clone(), fragments);1303 }1304 if let Some(link) = in_link.as_ref()1305 && let Ok(url) = Url::parse(link)1306 && (url.scheme() == "https" || url.scheme() == "http")1307 {1308 // Don't check the text associated with external URLs1309 continue;1310 }1311 text_to_check.push((text, range.clone(), code_level));1312 doc_suspicious_footnotes::check(cx, doc, range, &fragments, attrs);1313 }1314 }1315 FootnoteReference(_) => {}1316 }1317 }13181319 doc_comment_double_space_linebreaks::check(cx, &collected_breaks);13201321 headers1322}13231324fn looks_like_refdef(doc: &str, range: Range<usize>) -> Option<Range<usize>> {1325 if range.end < range.start {1326 return None;1327 }13281329 let offset = range.start;1330 let mut iterator = doc.as_bytes()[range].iter().copied().enumerate();1331 let mut start = None;1332 while let Some((i, byte)) = iterator.next() {1333 match byte {1334 b'\\' => {1335 iterator.next();1336 },1337 b'[' => {1338 start = Some(i + offset);1339 },1340 b']' if let Some(start) = start1341 && doc.as_bytes().get(i + offset + 1) == Some(&b':') =>1342 {1343 return Some(start..i + offset + 1);1344 },1345 _ => {},1346 }1347 }1348 None1349}