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::{is_entrypoint_fn, is_trait_impl_item};5use rustc_data_structures::fx::FxHashSet;6use rustc_errors::Applicability;7use rustc_hir::{Attribute, FieldDef, ImplItemKind, ItemKind, Node, Safety, TraitItemKind};8use rustc_lint::{EarlyContext, EarlyLintPass, LateContext, LateLintPass, LintContext as _, impl_lint_pass};9use rustc_resolve::rustdoc::pulldown_cmark::Event::{10 Code, DisplayMath, End, FootnoteReference, HardBreak, Html, InlineHtml, InlineMath, Rule, SoftBreak, Start,11 TaskListMarker, Text,12};13use rustc_resolve::rustdoc::pulldown_cmark::Tag::{14 BlockQuote, CodeBlock, FootnoteDefinition, Heading, Item, Link, Paragraph,15};16use rustc_resolve::rustdoc::pulldown_cmark::{BrokenLink, CodeBlockKind, CowStr, Options, TagEnd};17use rustc_resolve::rustdoc::{18 DocFragment, add_doc_fragment, attrs_to_doc_fragments, main_body_opts, pulldown_cmark,19 source_span_for_markdown_range, span_of_fragments,20};21use rustc_span::Span;22use std::ops::Range;23use url::Url;2425mod broken_link;26mod doc_comment_double_space_linebreaks;27mod doc_paragraphs_missing_punctuation;28mod doc_suspicious_footnotes;29mod include_in_doc_without_cfg;30mod lazy_continuation;31mod link_with_quotes;32mod markdown;33mod missing_headers;34mod needless_doctest_main;35mod suspicious_doc_comments;36mod test_attr_in_doctest;37mod too_long_first_doc_paragraph;3839declare_clippy_lint! {40 /// ### What it does41 /// Checks the doc comments have unbroken links, mostly caused42 /// by bad formatted links such as broken across multiple lines.43 ///44 /// ### Why is this bad?45 /// Because documentation generated by rustdoc will be broken46 /// since expected links won't be links and just text.47 ///48 /// ### Examples49 /// This link is broken:50 /// ```no_run51 /// /// [example of a bad link](https://52 /// /// github.com/rust-lang/rust-clippy/)53 /// pub fn do_something() {}54 /// ```55 ///56 /// It shouldn't be broken across multiple lines to work:57 /// ```no_run58 /// /// [example of a good link](https://github.com/rust-lang/rust-clippy/)59 /// pub fn do_something() {}60 /// ```61 #[clippy::version = "1.90.0"]62 pub DOC_BROKEN_LINK,63 pedantic,64 "broken document link"65}6667declare_clippy_lint! {68 /// ### What it does69 /// Detects doc comments that use double spaces as hard line break, instead of backslash (`\`).70 ///71 /// ### Why is this bad?72 /// Double spaces, when used as hard line break in doc comments, can be difficult to see, and may73 /// accidentally be removed during automatic formatting or manual refactoring. The use of a backslash (`\`)74 /// is clearer in this regard.75 ///76 /// ### Example77 /// The two replacement dots (`··`) in this example represent a double space.78 /// ```no_run79 /// /// This function adds two numbers and returns the result··80 /// /// Overflow can occur when the max value is exceeded.81 /// fn add(l: i32, r: i32) -> i32 {82 /// l + r83 /// }84 /// ```85 ///86 /// Use instead:87 /// ```no_run88 /// /// This function adds two numbers and returns the result\89 /// /// Overflow can occur when the max value is exceeded.90 /// fn add(l: i32, r: i32) -> i32 {91 /// l + r92 /// }93 /// ```94 #[clippy::version = "1.87.0"]95 pub DOC_COMMENT_DOUBLE_SPACE_LINEBREAKS,96 pedantic,97 "double space used for doc comment hard line break instead of `\\`"98}99100declare_clippy_lint! {101 /// ### What it does102 /// Checks if included files in doc comments are included only for `cfg(doc)`.103 ///104 /// ### Why restrict this?105 /// These files are not useful for compilation but will still be included.106 /// Also, if any of these non-source code file is updated, it will trigger a107 /// recompilation.108 ///109 /// ### Known problems110 ///111 /// Excluding this will currently result in the file being left out if112 /// the item's docs are inlined from another crate. This may be fixed in a113 /// future version of rustdoc.114 ///115 /// ### Example116 /// ```ignore117 /// #![doc = include_str!("some_file.md")]118 /// ```119 /// Use instead:120 /// ```no_run121 /// #![cfg_attr(doc, doc = include_str!("some_file.md"))]122 /// ```123 #[clippy::version = "1.85.0"]124 pub DOC_INCLUDE_WITHOUT_CFG,125 restriction,126 "check if files included in documentation are behind `cfg(doc)`"127}128129declare_clippy_lint! {130 /// ### What it does131 ///132 /// In CommonMark Markdown, the language used to write doc comments, a133 /// paragraph nested within a list or block quote does not need any line134 /// after the first one to be indented or marked. The specification calls135 /// this a "lazy paragraph continuation."136 ///137 /// ### Why is this bad?138 ///139 /// This is easy to write but hard to read. Lazy continuations makes140 /// unintended markers hard to see, and make it harder to deduce the141 /// document's intended structure.142 ///143 /// ### Example144 ///145 /// This table is probably intended to have two rows,146 /// but it does not. It has zero rows, and is followed by147 /// a block quote.148 /// ```no_run149 /// /// Range | Description150 /// /// ----- | -----------151 /// /// >= 1 | fully opaque152 /// /// < 1 | partially see-through153 /// fn set_opacity(opacity: f32) {}154 /// ```155 ///156 /// Fix it by escaping the marker:157 /// ```no_run158 /// /// Range | Description159 /// /// ----- | -----------160 /// /// \>= 1 | fully opaque161 /// /// < 1 | partially see-through162 /// fn set_opacity(opacity: f32) {}163 /// ```164 ///165 /// This example is actually intended to be a list:166 /// ```no_run167 /// /// * Do nothing.168 /// /// * Then do something. Whatever it is needs done,169 /// /// it should be done right now.170 /// # fn do_stuff() {}171 /// ```172 ///173 /// Fix it by indenting the list contents:174 /// ```no_run175 /// /// * Do nothing.176 /// /// * Then do something. Whatever it is needs done,177 /// /// it should be done right now.178 /// # fn do_stuff() {}179 /// ```180 #[clippy::version = "1.80.0"]181 pub DOC_LAZY_CONTINUATION,182 style,183 "require every line of a paragraph to be indented and marked"184}185186declare_clippy_lint! {187 /// ### What it does188 /// Checks for links with code directly adjacent to code text:189 /// `` [`MyItem`]`<`[`u32`]`>` ``.190 ///191 /// ### Why is this bad?192 /// It can be written more simply using HTML-style `<code>` tags.193 ///194 /// ### Example195 /// ```no_run196 /// //! [`first`](x)`second`197 /// ```198 /// Use instead:199 /// ```no_run200 /// //! <code>[first](x)second</code>201 /// ```202 #[clippy::version = "1.87.0"]203 pub DOC_LINK_CODE,204 nursery,205 "link with code back-to-back with other code"206}207208declare_clippy_lint! {209 /// ### What it does210 /// Detects the syntax `['foo']` in documentation comments (notice quotes instead of backticks)211 /// outside of code blocks212 /// ### Why is this bad?213 /// It is likely a typo when defining an intra-doc link214 ///215 /// ### Example216 /// ```no_run217 /// /// See also: ['foo']218 /// fn bar() {}219 /// ```220 /// Use instead:221 /// ```no_run222 /// /// See also: [`foo`]223 /// fn bar() {}224 /// ```225 #[clippy::version = "1.63.0"]226 pub DOC_LINK_WITH_QUOTES,227 pedantic,228 "possible typo for an intra-doc link"229}230231declare_clippy_lint! {232 /// ### What it does233 /// Checks for the presence of `_`, `::` or camel-case words234 /// outside ticks in documentation.235 ///236 /// ### Why is this bad?237 /// *Rustdoc* supports markdown formatting, `_`, `::` and238 /// camel-case probably indicates some code which should be included between239 /// ticks. `_` can also be used for emphasis in markdown, this lint tries to240 /// consider that.241 ///242 /// ### Known problems243 /// Lots of bad docs won’t be fixed, what the lint checks244 /// for is limited, and there are still false positives. HTML elements and their245 /// content are not linted.246 ///247 /// In addition, when writing documentation comments, including `[]` brackets248 /// inside a link text would trip the parser. Therefore, documenting link with249 /// `[`SmallVec<[T; INLINE_CAPACITY]>`]` and then [`SmallVec<[T; INLINE_CAPACITY]>`]: SmallVec250 /// would fail.251 ///252 /// ### Examples253 /// ```no_run254 /// /// Do something with the foo_bar parameter. See also255 /// /// that::other::module::foo.256 /// // ^ `foo_bar` and `that::other::module::foo` should be ticked.257 /// fn doit(foo_bar: usize) {}258 /// ```259 ///260 /// ```no_run261 /// // Link text with `[]` brackets should be written as following:262 /// /// Consume the array and return the inner263 /// /// [`SmallVec<[T; INLINE_CAPACITY]>`][SmallVec].264 /// /// [SmallVec]: SmallVec265 /// fn main() {}266 /// ```267 #[clippy::version = "pre 1.29.0"]268 pub DOC_MARKDOWN,269 pedantic,270 "presence of `_`, `::` or camel-case outside backticks in documentation"271}272273declare_clippy_lint! {274 /// ### What it does275 /// Warns if a link reference definition appears at the start of a276 /// list item or quote.277 ///278 /// ### Why is this bad?279 /// This is probably intended as an intra-doc link. If it is really280 /// supposed to be a reference definition, it can be written outside281 /// of the list item or quote.282 ///283 /// ### Example284 /// ```no_run285 /// //! - [link]: description286 /// ```287 /// Use instead:288 /// ```no_run289 /// //! - [link][]: description (for intra-doc link)290 /// //!291 /// //! [link]: destination (for link reference definition)292 /// ```293 #[clippy::version = "1.85.0"]294 pub DOC_NESTED_REFDEFS,295 suspicious,296 "link reference defined in list item or quote"297}298299declare_clippy_lint! {300 /// ### What it does301 ///302 /// Detects overindented list items in doc comments where the continuation303 /// lines are indented more than necessary.304 ///305 /// ### Why is this bad?306 ///307 /// Overindented list items in doc comments can lead to inconsistent and308 /// poorly formatted documentation when rendered. Excessive indentation may309 /// cause the text to be misinterpreted as a nested list item or code block,310 /// affecting readability and the overall structure of the documentation.311 ///312 /// ### Example313 ///314 /// ```no_run315 /// /// - This is the first item in a list316 /// /// and this line is overindented.317 /// # fn foo() {}318 /// ```319 ///320 /// Fixes this into:321 /// ```no_run322 /// /// - This is the first item in a list323 /// /// and this line is overindented.324 /// # fn foo() {}325 /// ```326 #[clippy::version = "1.86.0"]327 pub DOC_OVERINDENTED_LIST_ITEMS,328 style,329 "ensure list items are not overindented"330}331332declare_clippy_lint! {333 /// ### What it does334 /// Checks for doc comments whose paragraphs do not end with a period or another punctuation mark.335 /// Various Markdowns constructs are taken into account to avoid false positives.336 ///337 /// ### Why is this bad?338 /// A project may wish to enforce consistent doc comments by making sure paragraphs end with a339 /// punctuation mark.340 ///341 /// ### Example342 /// ```no_run343 /// /// Returns a random number344 /// ///345 /// /// It was chosen by a fair dice roll346 /// # fn foo() {}347 /// ```348 /// Use instead:349 /// ```no_run350 /// /// Returns a random number.351 /// ///352 /// /// It was chosen by a fair dice roll.353 /// # fn foo() {}354 /// ```355 ///356 /// ### Terminal punctuation marks357 /// This lint treats these characters as end markers: '.', '?', '!', '…' and ':'.358 ///359 /// The colon is not exactly a terminal punctuation mark, but this is required for paragraphs that360 /// introduce a table or a list for example.361 #[clippy::version = "1.93.0"]362 pub DOC_PARAGRAPHS_MISSING_PUNCTUATION,363 restriction,364 "missing terminal punctuation in doc comments"365}366367declare_clippy_lint! {368 /// ### What it does369 /// Detects syntax that looks like a footnote reference.370 ///371 /// Rustdoc footnotes are compatible with GitHub-Flavored Markdown (GFM).372 /// GFM does not parse a footnote reference unless its definition also373 /// exists. This lint checks for footnote references with missing374 /// definitions, unless it thinks you're writing a regex.375 ///376 /// ### Why is this bad?377 /// This probably means that a footnote was meant to exist,378 /// but was not written.379 ///380 /// ### Example381 /// ```no_run382 /// /// This is not a footnote[^1], because no definition exists.383 /// fn my_fn() {}384 /// ```385 /// Use instead:386 /// ```no_run387 /// /// This is a footnote[^1].388 /// ///389 /// /// [^1]: defined here390 /// fn my_fn() {}391 /// ```392 #[clippy::version = "1.89.0"]393 pub DOC_SUSPICIOUS_FOOTNOTES,394 suspicious,395 "looks like a link or footnote ref, but with no definition"396}397398declare_clippy_lint! {399 /// ### What it does400 /// Detects documentation that is empty.401 /// ### Why is this bad?402 /// Empty docs clutter code without adding value, reducing readability and maintainability.403 /// ### Example404 /// ```no_run405 /// ///406 /// fn returns_true() -> bool {407 /// true408 /// }409 /// ```410 /// Use instead:411 /// ```no_run412 /// fn returns_true() -> bool {413 /// true414 /// }415 /// ```416 #[clippy::version = "1.78.0"]417 pub EMPTY_DOCS,418 suspicious,419 "docstrings exist but documentation is empty"420}421422declare_clippy_lint! {423 /// ### What it does424 /// Checks the doc comments of publicly visible functions that425 /// return a `Result` type and warns if there is no `# Errors` section.426 ///427 /// ### Why is this bad?428 /// Documenting the type of errors that can be returned from a429 /// function can help callers write code to handle the errors appropriately.430 ///431 /// ### Examples432 /// Since the following function returns a `Result` it has an `# Errors` section in433 /// its doc comment:434 ///435 /// ```no_run436 ///# use std::io;437 /// /// # Errors438 /// ///439 /// /// Will return `Err` if `filename` does not exist or the user does not have440 /// /// permission to read it.441 /// pub fn read(filename: String) -> io::Result<String> {442 /// unimplemented!();443 /// }444 /// ```445 #[clippy::version = "1.41.0"]446 pub MISSING_ERRORS_DOC,447 pedantic,448 "`pub fn` returns `Result` without `# Errors` in doc comment"449}450451declare_clippy_lint! {452 /// ### What it does453 /// Checks the doc comments of publicly visible functions that454 /// may panic and warns if there is no `# Panics` section.455 ///456 /// ### Why is this bad?457 /// Documenting the scenarios in which panicking occurs458 /// can help callers who do not want to panic to avoid those situations.459 ///460 /// ### Examples461 /// Since the following function may panic it has a `# Panics` section in462 /// its doc comment:463 ///464 /// ```no_run465 /// /// # Panics466 /// ///467 /// /// Will panic if y is 0468 /// pub fn divide_by(x: i32, y: i32) -> i32 {469 /// if y == 0 {470 /// panic!("Cannot divide by 0")471 /// } else {472 /// x / y473 /// }474 /// }475 /// ```476 ///477 /// Individual panics within a function can be ignored with `#[expect]` or478 /// `#[allow]`:479 ///480 /// ```no_run481 /// # use std::num::NonZeroUsize;482 /// pub fn will_not_panic(x: usize) {483 /// #[expect(clippy::missing_panics_doc, reason = "infallible")]484 /// let y = NonZeroUsize::new(1).unwrap();485 ///486 /// // If any panics are added in the future the lint will still catch them487 /// }488 /// ```489 #[clippy::version = "1.51.0"]490 pub MISSING_PANICS_DOC,491 pedantic,492 "`pub fn` may panic without `# Panics` in doc comment"493}494495declare_clippy_lint! {496 /// ### What it does497 /// Checks for the doc comments of publicly visible498 /// unsafe functions and warns if there is no `# Safety` section.499 ///500 /// ### Why is this bad?501 /// Unsafe functions should document their safety502 /// preconditions, so that users can be sure they are using them safely.503 ///504 /// ### Examples505 /// ```no_run506 ///# type Universe = ();507 /// /// This function should really be documented508 /// pub unsafe fn start_apocalypse(u: &mut Universe) {509 /// unimplemented!();510 /// }511 /// ```512 ///513 /// At least write a line about safety:514 ///515 /// ```no_run516 ///# type Universe = ();517 /// /// # Safety518 /// ///519 /// /// This function should not be called before the horsemen are ready.520 /// pub unsafe fn start_apocalypse(u: &mut Universe) {521 /// unimplemented!();522 /// }523 /// ```524 #[clippy::version = "1.39.0"]525 pub MISSING_SAFETY_DOC,526 style,527 "`pub unsafe fn` without `# Safety` docs"528}529530declare_clippy_lint! {531 /// ### What it does532 /// Checks for `fn main() { .. }` in doctests533 ///534 /// ### Why is this bad?535 /// The test can be shorter (and likely more readable)536 /// if the `fn main()` is left implicit.537 ///538 /// ### Examples539 /// ```no_run540 /// /// An example of a doctest with a `main()` function541 /// ///542 /// /// # Examples543 /// ///544 /// /// ```545 /// /// fn main() {546 /// /// // this needs not be in an `fn`547 /// /// }548 /// /// ```549 /// fn needless_main() {550 /// unimplemented!();551 /// }552 /// ```553 #[clippy::version = "1.40.0"]554 pub NEEDLESS_DOCTEST_MAIN,555 style,556 "presence of `fn main() {` in code examples"557}558559declare_clippy_lint! {560 /// ### What it does561 /// Detects the use of outer doc comments (`///`, `/**`) followed by a bang (`!`): `///!`562 ///563 /// ### Why is this bad?564 /// Triple-slash comments (known as "outer doc comments") apply to items that follow it.565 /// An outer doc comment followed by a bang (i.e. `///!`) has no specific meaning.566 ///567 /// The user most likely meant to write an inner doc comment (`//!`, `/*!`), which568 /// applies to the parent item (i.e. the item that the comment is contained in,569 /// usually a module or crate).570 ///571 /// ### Known problems572 /// Inner doc comments can only appear before items, so there are certain cases where the suggestion573 /// made by this lint is not valid code. For example:574 /// ```rust575 /// fn foo() {}576 /// ///!577 /// fn bar() {}578 /// ```579 /// This lint detects the doc comment and suggests changing it to `//!`, but an inner doc comment580 /// is not valid at that position.581 ///582 /// ### Example583 /// In this example, the doc comment is attached to the *function*, rather than the *module*.584 /// ```no_run585 /// pub mod util {586 /// ///! This module contains utility functions.587 ///588 /// pub fn dummy() {}589 /// }590 /// ```591 ///592 /// Use instead:593 /// ```no_run594 /// pub mod util {595 /// //! This module contains utility functions.596 ///597 /// pub fn dummy() {}598 /// }599 /// ```600 #[clippy::version = "1.70.0"]601 pub SUSPICIOUS_DOC_COMMENTS,602 suspicious,603 "suspicious usage of (outer) doc comments"604}605606declare_clippy_lint! {607 /// ### What it does608 /// Checks for `#[test]` in doctests unless they are marked with609 /// either `ignore`, `no_run` or `compile_fail`.610 ///611 /// ### Why is this bad?612 /// Code in examples marked as `#[test]` will somewhat613 /// surprisingly not be run by `cargo test`. If you really want614 /// to show how to test stuff in an example, mark it `no_run` to615 /// make the intent clear.616 ///617 /// ### Examples618 /// ```no_run619 /// /// An example of a doctest with a `main()` function620 /// ///621 /// /// # Examples622 /// ///623 /// /// ```624 /// /// #[test]625 /// /// fn equality_works() {626 /// /// assert_eq!(1_u8, 1);627 /// /// }628 /// /// ```629 /// fn test_attr_in_doctest() {630 /// unimplemented!();631 /// }632 /// ```633 #[clippy::version = "1.76.0"]634 pub TEST_ATTR_IN_DOCTEST,635 suspicious,636 "presence of `#[test]` in code examples"637}638639declare_clippy_lint! {640 /// ### What it does641 /// Checks if the first paragraph in the documentation of items listed in the module page is too long.642 ///643 /// ### Why is this bad?644 /// Documentation will show the first paragraph of the docstring in the summary page of a645 /// module. Having a nice, short summary in the first paragraph is part of writing good docs.646 ///647 /// ### Example648 /// ```no_run649 /// /// A very short summary.650 /// /// A much longer explanation that goes into a lot more detail about651 /// /// how the thing works, possibly with doclinks and so one,652 /// /// and probably spanning a many rows.653 /// struct Foo {}654 /// ```655 /// Use instead:656 /// ```no_run657 /// /// A very short summary.658 /// ///659 /// /// A much longer explanation that goes into a lot more detail about660 /// /// how the thing works, possibly with doclinks and so one,661 /// /// and probably spanning a many rows.662 /// struct Foo {}663 /// ```664 #[clippy::version = "1.82.0"]665 pub TOO_LONG_FIRST_DOC_PARAGRAPH,666 nursery,667 "ensure the first documentation paragraph is short"668}669670declare_clippy_lint! {671 /// ### What it does672 /// Checks for the doc comments of publicly visible673 /// safe functions and traits and warns if there is a `# Safety` section.674 ///675 /// ### Why restrict this?676 /// Safe functions and traits are safe to implement and therefore do not677 /// need to describe safety preconditions that users are required to uphold.678 ///679 /// ### Examples680 /// ```no_run681 ///# type Universe = ();682 /// /// # Safety683 /// ///684 /// /// This function should not be called before the horsemen are ready.685 /// pub fn start_apocalypse_but_safely(u: &mut Universe) {686 /// unimplemented!();687 /// }688 /// ```689 ///690 /// The function is safe, so there shouldn't be any preconditions691 /// that have to be explained for safety reasons.692 ///693 /// ```no_run694 ///# type Universe = ();695 /// /// This function should really be documented696 /// pub fn start_apocalypse(u: &mut Universe) {697 /// unimplemented!();698 /// }699 /// ```700 #[clippy::version = "1.67.0"]701 pub UNNECESSARY_SAFETY_DOC,702 restriction,703 "`pub fn` or `pub trait` with `# Safety` docs"704}705706impl_lint_pass!(Documentation => [707 DOC_BROKEN_LINK,708 DOC_COMMENT_DOUBLE_SPACE_LINEBREAKS,709 DOC_INCLUDE_WITHOUT_CFG,710 DOC_LAZY_CONTINUATION,711 DOC_LINK_CODE,712 DOC_LINK_WITH_QUOTES,713 DOC_MARKDOWN,714 DOC_NESTED_REFDEFS,715 DOC_OVERINDENTED_LIST_ITEMS,716 DOC_PARAGRAPHS_MISSING_PUNCTUATION,717 DOC_SUSPICIOUS_FOOTNOTES,718 EMPTY_DOCS,719 MISSING_ERRORS_DOC,720 MISSING_PANICS_DOC,721 MISSING_SAFETY_DOC,722 NEEDLESS_DOCTEST_MAIN,723 SUSPICIOUS_DOC_COMMENTS,724 TEST_ATTR_IN_DOCTEST,725 TOO_LONG_FIRST_DOC_PARAGRAPH,726 UNNECESSARY_SAFETY_DOC,727]);728729pub struct Documentation {730 valid_idents: &'static FxHashSet<String>,731 check_private_items: bool,732}733734impl Documentation {735 pub fn new(conf: &'static Conf) -> Self {736 Self {737 valid_idents: &conf.doc_valid_idents,738 check_private_items: conf.check_private_items,739 }740 }741}742743impl EarlyLintPass for Documentation {744 fn check_attributes(&mut self, cx: &EarlyContext<'_>, attrs: &[rustc_ast::Attribute]) {745 include_in_doc_without_cfg::check(cx, attrs);746 }747}748749impl<'tcx> LateLintPass<'tcx> for Documentation {750 fn check_attributes(&mut self, cx: &LateContext<'tcx>, attrs: &'tcx [Attribute]) {751 let Some(headers) = check_attrs(cx, self.valid_idents, attrs) else {752 return;753 };754755 match cx.tcx.hir_node(cx.last_node_with_lint_attrs) {756 Node::Field(FieldDef { span, safety, .. }) => match (headers.safety, safety) {757 (false, Safety::Unsafe) => span_lint(758 cx,759 MISSING_SAFETY_DOC,760 *span,761 "docs for unsafe field missing `# Safety` section",762 ),763 (true, Safety::Safe) if cx.tcx.features().unsafe_fields() => span_lint_and_help(764 cx,765 UNNECESSARY_SAFETY_DOC,766 *span,767 "field with `# Safety` documentation is not marked unsafe",768 None,769 "if the field has safety invariants, mark it `unsafe`",770 ),771 _ => (),772 },773 Node::Item(item) => {774 too_long_first_doc_paragraph::check(775 cx,776 item,777 attrs,778 headers.first_paragraph_text_len,779 headers.first_paragraph_md_len,780 self.check_private_items,781 );782 match item.kind {783 ItemKind::Fn { sig, body, .. }784 if !(is_entrypoint_fn(cx, item.owner_id.to_def_id())785 || item.span.in_external_macro(cx.tcx.sess.source_map())) =>786 {787 missing_headers::check(cx, item.owner_id, sig, headers, Some(body), self.check_private_items);788 },789 ItemKind::Trait { safety, .. } => match (headers.safety, safety) {790 (false, Safety::Unsafe) => span_lint(791 cx,792 MISSING_SAFETY_DOC,793 cx.tcx.def_span(item.owner_id),794 "docs for unsafe trait missing `# Safety` section",795 ),796 (true, Safety::Safe) => span_lint(797 cx,798 UNNECESSARY_SAFETY_DOC,799 cx.tcx.def_span(item.owner_id),800 "docs for safe trait have unnecessary `# Safety` section",801 ),802 _ => (),803 },804 _ => (),805 }806 },807 Node::TraitItem(trait_item) => {808 if let TraitItemKind::Fn(sig, ..) = trait_item.kind809 && !trait_item.span.in_external_macro(cx.tcx.sess.source_map())810 {811 missing_headers::check(cx, trait_item.owner_id, sig, headers, None, self.check_private_items);812 }813 },814 Node::ImplItem(impl_item) => {815 if let ImplItemKind::Fn(sig, body_id) = impl_item.kind816 && !impl_item.span.in_external_macro(cx.tcx.sess.source_map())817 && !is_trait_impl_item(cx, impl_item.hir_id())818 {819 missing_headers::check(820 cx,821 impl_item.owner_id,822 sig,823 headers,824 Some(body_id),825 self.check_private_items,826 );827 }828 },829 _ => {},830 }831 }832}833834#[derive(Copy, Clone)]835struct Fragments<'a> {836 doc: &'a str,837 fragments: &'a [DocFragment],838}839840impl Fragments<'_> {841 /// get the span for the markdown range. Note that this function is not cheap, use it with842 /// caution.843 #[must_use]844 fn span(self, cx: &LateContext<'_>, range: Range<usize>) -> Option<Span> {845 source_span_for_markdown_range(cx.tcx, self.doc, &range, self.fragments).map(|(sp, _)| sp)846 }847}848849#[derive(Copy, Clone, Default)]850struct DocHeaders {851 safety: bool,852 errors: bool,853 panics: bool,854 first_paragraph_md_len: usize,855 first_paragraph_text_len: usize,856}857858/// Does some pre-processing on raw, desugared `#[doc]` attributes such as parsing them and859/// then delegates to `check_doc`.860/// Some lints are already checked here if they can work with attributes directly and don't need861/// to work with markdown.862/// Others are checked elsewhere, e.g. in `check_doc` if they need access to markdown, or863/// back in the various late lint pass methods if they need the final doc headers, like "Safety" or864/// "Panics" sections.865fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[Attribute]) -> Option<DocHeaders> {866 // We don't want the parser to choke on intra doc links. Since we don't867 // actually care about rendering them, just pretend that all broken links868 // point to a fake address.869 #[expect(clippy::unnecessary_wraps)] // we're following a type signature870 fn fake_broken_link_callback<'a>(_: BrokenLink<'_>) -> Option<(CowStr<'a>, CowStr<'a>)> {871 Some(("fake".into(), "fake".into()))872 }873874 if suspicious_doc_comments::check(cx, attrs) || is_doc_hidden(attrs) {875 return None;876 }877878 let (fragments, _) = attrs_to_doc_fragments(879 attrs.iter().filter_map(|attr| {880 if attr.doc_str_and_fragment_kind().is_none() || attr.span().in_external_macro(cx.sess().source_map()) {881 None882 } else {883 Some((attr, None))884 }885 }),886 true,887 );888889 let mut doc = String::with_capacity(fragments.iter().map(|frag| frag.doc.as_str().len() + 1).sum());890891 for fragment in &fragments {892 add_doc_fragment(&mut doc, fragment);893 }894 doc.pop();895896 if doc.trim().is_empty() {897 if let Some(span) = span_of_fragments(&fragments) {898 span_lint_and_help(899 cx,900 EMPTY_DOCS,901 span,902 "empty doc comment",903 None,904 "consider removing or filling it",905 );906 }907 return Some(DocHeaders::default());908 }909910 // Only emits the allow-by-default `DOC_LINK_CODE`; skip its extra markdown reparse when it's off.911 if !clippy_utils::is_lint_allowed(cx, DOC_LINK_CODE, cx.last_node_with_lint_attrs) {912 check_for_code_clusters(913 cx,914 pulldown_cmark::Parser::new_with_broken_link_callback(915 &doc,916 main_body_opts() - Options::ENABLE_SMART_PUNCTUATION,917 Some(&mut fake_broken_link_callback),918 )919 .into_offset_iter(),920 &doc,921 Fragments {922 doc: &doc,923 fragments: &fragments,924 },925 );926 }927928 // Same for the allow-by-default `DOC_PARAGRAPHS_MISSING_PUNCTUATION`, which also reparses.929 if !clippy_utils::is_lint_allowed(cx, DOC_PARAGRAPHS_MISSING_PUNCTUATION, cx.last_node_with_lint_attrs) {930 doc_paragraphs_missing_punctuation::check(931 cx,932 &doc,933 Fragments {934 doc: &doc,935 fragments: &fragments,936 },937 );938 }939940 // NOTE: check_doc uses it own cb function,941 // to avoid causing duplicated diagnostics for the broken link checker.942 let mut full_fake_broken_link_callback = |bl: BrokenLink<'_>| -> Option<(CowStr<'_>, CowStr<'_>)> {943 broken_link::check(cx, &bl, &doc, &fragments);944 Some(("fake".into(), "fake".into()))945 };946947 // disable smart punctuation to pick up ['link'] more easily948 let opts = main_body_opts() - Options::ENABLE_SMART_PUNCTUATION;949 let parser =950 pulldown_cmark::Parser::new_with_broken_link_callback(&doc, opts, Some(&mut full_fake_broken_link_callback));951952 Some(check_doc(953 cx,954 valid_idents,955 parser.into_offset_iter(),956 &doc,957 Fragments {958 doc: &doc,959 fragments: &fragments,960 },961 attrs,962 ))963}964965enum Container {966 Blockquote,967 List(usize),968}969970/// Scan the documentation for code links that are back-to-back with code spans.971///972/// This is done separately from the rest of the docs, because that makes it easier to produce973/// the correct messages.974fn check_for_code_clusters<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize>)>>(975 cx: &LateContext<'_>,976 events: Events,977 doc: &str,978 fragments: Fragments<'_>,979) {980 let mut events = events.peekable();981 let mut code_starts_at = None;982 let mut code_ends_at = None;983 let mut code_includes_link = false;984 while let Some((event, range)) = events.next() {985 match event {986 Start(Link { .. }) if matches!(events.peek(), Some((Code(_), _range))) => {987 if code_starts_at.is_some() {988 code_ends_at = Some(range.end);989 } else {990 code_starts_at = Some(range.start);991 }992 code_includes_link = true;993 // skip the nested "code", because we're already handling it here994 let _ = events.next();995 },996 Code(_) => {997 if code_starts_at.is_some() {998 code_ends_at = Some(range.end);999 } else {1000 code_starts_at = Some(range.start);1001 }1002 },1003 End(TagEnd::Link) => {},1004 _ => {1005 if let Some(start) = code_starts_at1006 && let Some(end) = code_ends_at1007 && code_includes_link1008 && let Some(span) = fragments.span(cx, start..end)1009 {1010 span_lint_and_then(cx, DOC_LINK_CODE, span, "code link adjacent to code text", |diag| {1011 let sugg = format!("<code>{}</code>", doc[start..end].replace('`', ""));1012 diag.span_suggestion_verbose(1013 span,1014 "wrap the entire group in `<code>` tags",1015 sugg,1016 Applicability::MaybeIncorrect,1017 );1018 diag.help("separate code snippets will be shown with a gap");1019 });1020 }1021 code_includes_link = false;1022 code_starts_at = None;1023 code_ends_at = None;1024 },1025 }1026 }1027}10281029#[derive(Clone, Copy)]1030#[expect(clippy::struct_excessive_bools)]1031struct CodeTags {1032 no_run: bool,1033 ignore: bool,1034 compile_fail: bool,1035 test_harness: bool,10361037 rust: bool,1038}10391040impl Default for CodeTags {1041 fn default() -> Self {1042 Self {1043 no_run: false,1044 ignore: false,1045 compile_fail: false,1046 test_harness: false,10471048 rust: true,1049 }1050 }1051}10521053impl CodeTags {1054 /// Based on <https://github.com/rust-lang/rust/blob/1.90.0/src/librustdoc/html/markdown.rs#L1169>1055 fn parse(lang: &str) -> Self {1056 let mut tags = Self::default();10571058 let mut seen_rust_tags = false;1059 let mut seen_other_tags = false;1060 for item in lang.split([',', ' ', '\t']) {1061 match item.trim() {1062 "" => {},1063 "rust" => {1064 tags.rust = true;1065 seen_rust_tags = true;1066 },1067 "ignore" => {1068 tags.ignore = true;1069 seen_rust_tags = !seen_other_tags;1070 },1071 "no_run" => {1072 tags.no_run = true;1073 seen_rust_tags = !seen_other_tags;1074 },1075 "should_panic" => seen_rust_tags = !seen_other_tags,1076 "compile_fail" => {1077 tags.compile_fail = true;1078 seen_rust_tags = !seen_other_tags || seen_rust_tags;1079 },1080 "test_harness" => {1081 tags.test_harness = true;1082 seen_rust_tags = !seen_other_tags || seen_rust_tags;1083 },1084 "standalone_crate" => {1085 seen_rust_tags = !seen_other_tags || seen_rust_tags;1086 },1087 _ if item.starts_with("ignore-") => seen_rust_tags = true,1088 _ if item.starts_with("edition") => {},1089 _ => seen_other_tags = true,1090 }1091 }10921093 tags.rust &= seen_rust_tags || !seen_other_tags;10941095 tags1096 }1097}10981099/// Checks parsed documentation.1100/// This walks the "events" (think sections of markdown) produced by `pulldown_cmark`,1101/// so lints here will generally access that information.1102/// Returns documentation headers -- whether a "Safety", "Errors", "Panic" section was found1103#[expect(clippy::too_many_lines, reason = "big match statement")]1104fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize>)>>(1105 cx: &LateContext<'_>,1106 valid_idents: &FxHashSet<String>,1107 events: Events,1108 doc: &str,1109 fragments: Fragments<'_>,1110 attrs: &[Attribute],1111) -> DocHeaders {1112 // true if a safety header was found1113 let mut headers = DocHeaders::default();1114 let mut code = None;1115 let mut in_link = None;1116 let mut in_heading = false;1117 let mut in_footnote_definition = false;1118 let mut ticks_unbalanced = false;1119 let mut text_to_check: Vec<(CowStr<'_>, Range<usize>, isize)> = Vec::new();1120 let mut paragraph_range = 0..0;1121 let mut code_level = 0;1122 let mut blockquote_level = 0;1123 let mut collected_breaks: Vec<Span> = Vec::new();1124 let mut is_first_paragraph = true;11251126 let mut containers = Vec::new();11271128 // Skip collecting text and the per-word scan when `DOC_MARKDOWN` (pedantic) is allowed.1129 let check_doc_markdown = !clippy_utils::is_lint_allowed(cx, DOC_MARKDOWN, cx.last_node_with_lint_attrs);11301131 let mut events = events.peekable();11321133 while let Some((event, range)) = events.next() {1134 match event {1135 Html(tag) | InlineHtml(tag) => {1136 if tag.starts_with("<code") {1137 code_level += 1;1138 } else if tag.starts_with("</code") {1139 code_level -= 1;1140 } else if tag.starts_with("<blockquote") || tag.starts_with("<q") {1141 blockquote_level += 1;1142 } else if tag.starts_with("</blockquote") || tag.starts_with("</q") {1143 blockquote_level -= 1;1144 }1145 },1146 Start(BlockQuote(_)) => {1147 blockquote_level += 1;1148 containers.push(Container::Blockquote);1149 if let Some((next_event, next_range)) = events.peek() {1150 let next_start = match next_event {1151 End(TagEnd::BlockQuote) => next_range.end,1152 _ => next_range.start,1153 };1154 if let Some(refdefrange) = looks_like_refdef(doc, range.start..next_start) &&1155 let Some(refdefspan) = fragments.span(cx, refdefrange.clone())1156 {1157 span_lint_and_then(1158 cx,1159 DOC_NESTED_REFDEFS,1160 refdefspan,1161 "link reference defined in quote",1162 |diag| {1163 diag.span_suggestion_short(1164 refdefspan.shrink_to_hi(),1165 "for an intra-doc link, add `[]` between the label and the colon",1166 "[]",1167 Applicability::MaybeIncorrect,1168 );1169 diag.help("link definitions are not shown in rendered documentation");1170 }1171 );1172 }1173 }1174 },1175 End(TagEnd::BlockQuote) => {1176 blockquote_level -= 1;1177 containers.pop();1178 },1179 Start(CodeBlock(ref kind)) => {1180 code = Some(match kind {1181 CodeBlockKind::Indented => CodeTags::default(),1182 CodeBlockKind::Fenced(lang) => CodeTags::parse(lang),1183 });1184 },1185 End(TagEnd::CodeBlock) => code = None,1186 Start(Link { dest_url, .. }) => in_link = Some(dest_url),1187 End(TagEnd::Link) => in_link = None,1188 Start(Heading { .. } | Paragraph | Item) => {1189 if let Start(Heading { .. }) = event {1190 in_heading = true;1191 }1192 if let Start(Item) = event {1193 let indent = if let Some((next_event, next_range)) = events.peek() {1194 let next_start = match next_event {1195 End(TagEnd::Item) => next_range.end,1196 _ => next_range.start,1197 };1198 if let Some(refdefrange) = looks_like_refdef(doc, range.start..next_start) &&1199 let Some(refdefspan) = fragments.span(cx, refdefrange.clone())1200 {1201 span_lint_and_then(1202 cx,1203 DOC_NESTED_REFDEFS,1204 refdefspan,1205 "link reference defined in list item",1206 |diag| {1207 diag.span_suggestion_short(1208 refdefspan.shrink_to_hi(),1209 "for an intra-doc link, add `[]` between the label and the colon",1210 "[]",1211 Applicability::MaybeIncorrect,1212 );1213 diag.help("link definitions are not shown in rendered documentation");1214 }1215 );1216 refdefrange.start - range.start1217 } else {1218 let mut start = next_range.start;1219 if start > 0 && doc.as_bytes().get(start - 1) == Some(&b'\\') {1220 // backslashes aren't in the event stream...1221 start -= 1;1222 }12231224 start.saturating_sub(range.start)1225 }1226 } else {1227 01228 };1229 containers.push(Container::List(indent));1230 }1231 ticks_unbalanced = false;1232 paragraph_range = range;1233 if is_first_paragraph {1234 headers.first_paragraph_md_len = doc[paragraph_range.clone()].chars().count();1235 }1236 },1237 End(TagEnd::Heading(_) | TagEnd::Paragraph | TagEnd::Item) => {1238 if is_first_paragraph {1239 is_first_paragraph = false;1240 }1241 if let End(TagEnd::Heading(_)) = event {1242 in_heading = false;1243 }1244 if let End(TagEnd::Item) = event {1245 containers.pop();1246 }1247 if check_doc_markdown {1248 if ticks_unbalanced && let Some(span) = fragments.span(cx, paragraph_range.clone())1249 .or_else(|| span_of_fragments(fragments.fragments)) {1250 span_lint_and_help(1251 cx,1252 DOC_MARKDOWN,1253 span,1254 "backticks are unbalanced",1255 None,1256 "a backtick may be missing a pair",1257 );1258 text_to_check.clear();1259 } else {1260 for (text, range, assoc_code_level) in text_to_check.drain(..) {1261 markdown::check(1262 cx, valid_idents, &text, &fragments, range, assoc_code_level, blockquote_level1263 );1264 }1265 }1266 }1267 },1268 Start(FootnoteDefinition(..)) => in_footnote_definition = true,1269 End(TagEnd::FootnoteDefinition) => in_footnote_definition = false,1270 Start(_) | End(_) // We don't care about other tags1271 | TaskListMarker(_) | Rule => (),1272 SoftBreak | HardBreak => {1273 if is_first_paragraph {1274 headers.first_paragraph_text_len += 1;1275 }1276 if !containers.is_empty()1277 && !in_footnote_definition1278 // Tabs aren't handled correctly vvvv1279 && !doc[range.clone()].contains('\t')1280 && let Some((next_event, next_range)) = events.peek()1281 && !matches!(next_event, End(_))1282 {1283 lazy_continuation::check(1284 cx,1285 doc,1286 range.end..next_range.start,1287 &fragments,1288 &containers[..],1289 );1290 }129112921293 if event == HardBreak1294 && !doc[range.clone()].trim().starts_with('\\')1295 && let Some(span) = fragments.span(cx, range.clone())1296 && !span.from_expansion()1297 {1298 collected_breaks.push(span);1299 }1300 },1301 Code(code) | InlineMath(code) | DisplayMath(code) => {1302 if is_first_paragraph {1303 headers.first_paragraph_text_len += code.chars().count();1304 }1305 }1306 Text(text) => {1307 if is_first_paragraph {1308 headers.first_paragraph_text_len += text.chars().count();1309 }1310 paragraph_range.end = range.end;1311 let range_ = range.clone();1312 ticks_unbalanced |= text.contains('`')1313 && code.is_none()1314 && doc[range.clone()].bytes().enumerate().any(|(i, c)| {1315 // scan the markdown source code bytes for backquotes that aren't preceded by backslashes1316 // - use bytes, instead of chars, to avoid utf8 decoding overhead (special chars are ascii)1317 // - relevant backquotes are within doc[range], but backslashes are not, because they're not1318 // actually part of the rendered text (pulldown-cmark doesn't emit any events for escapes)1319 // - if `range_.start + i == 0`, then `range_.start + i - 1 == -1`, and since we're working in1320 // usize, that would underflow and maybe panic1321 c == b'`' && (range_.start + i == 0 || doc.as_bytes().get(range_.start + i - 1) != Some(&b'\\'))1322 });1323 if Some(&text) == in_link.as_ref() || ticks_unbalanced {1324 // Probably a link of the form `<http://example.com>`1325 // Which are represented as a link to "http://example.com" with1326 // text "http://example.com" by pulldown-cmark1327 continue;1328 }1329 let trimmed_text = text.trim();1330 headers.safety |= in_heading && trimmed_text == "Safety";1331 headers.safety |= in_heading && trimmed_text == "SAFETY";1332 headers.safety |= in_heading && trimmed_text == "Implementation safety";1333 headers.safety |= in_heading && trimmed_text == "Implementation Safety";1334 headers.errors |= in_heading && trimmed_text == "Errors";1335 headers.panics |= in_heading && trimmed_text == "Panics";13361337 if let Some(tags) = code {1338 if tags.rust && !tags.compile_fail && !tags.ignore {1339 needless_doctest_main::check(cx, &text, range.start, fragments);13401341 if !tags.no_run && !tags.test_harness {1342 test_attr_in_doctest::check(cx, &text, range.start, fragments);1343 }1344 }1345 } else {1346 if in_link.is_some() {1347 link_with_quotes::check(cx, trimmed_text, range.clone(), fragments);1348 }1349 if let Some(link) = in_link.as_ref()1350 && let Ok(url) = Url::parse(link)1351 && (url.scheme() == "https" || url.scheme() == "http")1352 {1353 // Don't check the text associated with external URLs1354 continue;1355 }1356 if check_doc_markdown {1357 text_to_check.push((text, range.clone(), code_level));1358 }1359 doc_suspicious_footnotes::check(cx, doc, range, &fragments, attrs);1360 }1361 }1362 FootnoteReference(_) => {}1363 }1364 }13651366 doc_comment_double_space_linebreaks::check(cx, &collected_breaks);13671368 headers1369}13701371fn looks_like_refdef(doc: &str, range: Range<usize>) -> Option<Range<usize>> {1372 if range.end < range.start {1373 return None;1374 }13751376 let offset = range.start;1377 let mut iterator = doc.as_bytes()[range].iter().copied().enumerate();1378 let mut start = None;1379 while let Some((i, byte)) = iterator.next() {1380 match byte {1381 b'\\' => {1382 iterator.next();1383 },1384 b'[' => {1385 start = Some(i + offset);1386 },1387 b']' if let Some(start) = start1388 && doc.as_bytes().get(i + offset + 1) == Some(&b':') =>1389 {1390 return Some(start..i + offset + 1);1391 },1392 _ => {},1393 }1394 }1395 None1396}
Findings
✓ No findings reported for this file.