src/tools/clippy/clippy_lints/src/doc/mod.rs RUST 1,397 lines View on github.com → Search inside
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.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.