From 40f7f31832f26a033a2edd7f2d5665452f766238 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sun, 27 Sep 2026 13:08:27 +0200 Subject: [PATCH] Add `doc_examples_missing_item` lint --- CHANGELOG.md | 1 + book/src/lint_configuration.md | 1 + clippy_config/src/conf.rs | 8 +- clippy_lints/src/declared_lints.rs | 1 + .../src/doc/doc_examples_missing_item.rs | 50 ++++ clippy_lints/src/doc/missing_headers.rs | 18 +- clippy_lints/src/doc/mod.rs | 75 +++++- clippy_utils/src/lib.rs | 5 + tests/ui-toml/private-doc-errors/doc_lints.rs | 23 ++ .../private-doc-errors/doc_lints.stderr | 43 +++- .../doc_examples_missing_item.rs | 234 ++++++++++++++++++ .../doc_examples_missing_item.stderr | 113 +++++++++ .../doc_examples_missing_item_proc_macro.rs | 14 ++ 13 files changed, 557 insertions(+), 29 deletions(-) create mode 100644 clippy_lints/src/doc/doc_examples_missing_item.rs create mode 100644 tests/ui/doc_examples_missing_item/doc_examples_missing_item.rs create mode 100644 tests/ui/doc_examples_missing_item/doc_examples_missing_item.stderr create mode 100644 tests/ui/doc_examples_missing_item/doc_examples_missing_item_proc_macro.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index dd35c0a1136c..4326a0c90fea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7075,6 +7075,7 @@ Released 2018-09-13 [`diverging_sub_expression`]: https://rust-lang.github.io/rust-clippy/main/index.html#diverging_sub_expression [`doc_broken_link`]: https://rust-lang.github.io/rust-clippy/main/index.html#doc_broken_link [`doc_comment_double_space_linebreaks`]: https://rust-lang.github.io/rust-clippy/main/index.html#doc_comment_double_space_linebreaks +[`doc_examples_missing_item`]: https://rust-lang.github.io/rust-clippy/main/index.html#doc_examples_missing_item [`doc_include_without_cfg`]: https://rust-lang.github.io/rust-clippy/main/index.html#doc_include_without_cfg [`doc_lazy_continuation`]: https://rust-lang.github.io/rust-clippy/main/index.html#doc_lazy_continuation [`doc_link_code`]: https://rust-lang.github.io/rust-clippy/main/index.html#doc_link_code diff --git a/book/src/lint_configuration.md b/book/src/lint_configuration.md index dd754a5c0166..231640ee8575 100644 --- a/book/src/lint_configuration.md +++ b/book/src/lint_configuration.md @@ -527,6 +527,7 @@ Whether to also run the listed lints on private items. --- **Affected lints:** +* [`doc_examples_missing_item`](https://rust-lang.github.io/rust-clippy/main/index.html#doc_examples_missing_item) * [`missing_errors_doc`](https://rust-lang.github.io/rust-clippy/main/index.html#missing_errors_doc) * [`missing_panics_doc`](https://rust-lang.github.io/rust-clippy/main/index.html#missing_panics_doc) * [`missing_safety_doc`](https://rust-lang.github.io/rust-clippy/main/index.html#missing_safety_doc) diff --git a/clippy_config/src/conf.rs b/clippy_config/src/conf.rs index 5bbde1922937..3b5fe4ee72de 100644 --- a/clippy_config/src/conf.rs +++ b/clippy_config/src/conf.rs @@ -474,7 +474,13 @@ define_Conf! { #[lints(inconsistent_struct_constructor)] check_inconsistent_struct_field_initializers("check-inconsistent-struct-field-initializers"): bool = false, /// Whether to also run the listed lints on private items. - #[lints(missing_errors_doc, missing_panics_doc, missing_safety_doc, unnecessary_safety_doc)] + #[lints( + doc_examples_missing_item, + missing_errors_doc, + missing_panics_doc, + missing_safety_doc, + unnecessary_safety_doc, + )] check_private_items("check-private-items"): bool = false, /// The maximum cognitive complexity a function can have #[lints(cognitive_complexity)] diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index 9b4ca2c979b8..3a7a54008b78 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -118,6 +118,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::disallowed_types::DISALLOWED_TYPES_INFO, crate::doc::DOC_BROKEN_LINK_INFO, crate::doc::DOC_COMMENT_DOUBLE_SPACE_LINEBREAKS_INFO, + crate::doc::DOC_EXAMPLES_MISSING_ITEM_INFO, crate::doc::DOC_INCLUDE_WITHOUT_CFG_INFO, crate::doc::DOC_LAZY_CONTINUATION_INFO, crate::doc::DOC_LINK_CODE_INFO, diff --git a/clippy_lints/src/doc/doc_examples_missing_item.rs b/clippy_lints/src/doc/doc_examples_missing_item.rs new file mode 100644 index 000000000000..7de488b84862 --- /dev/null +++ b/clippy_lints/src/doc/doc_examples_missing_item.rs @@ -0,0 +1,50 @@ +use super::{DOC_EXAMPLES_MISSING_ITEM, is_public_api}; +use clippy_utils::attrs::is_proc_macro; +use clippy_utils::diagnostics::span_lint_and_help; +use clippy_utils::{is_lint_allowed, is_trait_impl_item, tokenize_with_text}; +use rustc_hir::def::DefKind; +use rustc_lexer::TokenKind; +use rustc_lint::LateContext; +use rustc_span::Ident; + +pub(super) fn item_ident(cx: &LateContext<'_>, check_private_items: bool) -> Option { + let hir_id = cx.last_node_with_lint_attrs; + let owner_id = hir_id.as_owner()?; + if !is_lint_allowed(cx, DOC_EXAMPLES_MISSING_ITEM, hir_id) + // Types and traits are skipped because examples often use them through a glob import without naming + // them, and telling which glob-imported names an example uses needs name resolution on every doctest. + && matches!( + cx.tcx.def_kind(owner_id), + DefKind::Fn | DefKind::AssocFn | DefKind::Const | DefKind::AssocConst | DefKind::Static { .. } + ) + && !is_trait_impl_item(cx, hir_id) + && !is_proc_macro(cx.tcx.hir_attrs(hir_id)) + && (check_private_items || is_public_api(cx, owner_id)) + { + cx.tcx.opt_item_ident(owner_id.to_def_id()) + } else { + None + } +} + +pub(super) fn mentions(code: &str, ident: Ident) -> bool { + if !code.contains(ident.as_str()) { + return false; + } + + tokenize_with_text(code).any(|(kind, text, _)| { + matches!(kind, TokenKind::Ident | TokenKind::RawIdent) + && text.strip_prefix("r#").unwrap_or(text) == ident.as_str() + }) +} + +pub(super) fn report(cx: &LateContext<'_>, ident: Ident) { + span_lint_and_help( + cx, + DOC_EXAMPLES_MISSING_ITEM, + cx.tcx.def_span(cx.last_node_with_lint_attrs.owner), + format!("none of the documentation examples mention `{ident}`"), + None, + "consider adding an example that uses this item", + ); +} diff --git a/clippy_lints/src/doc/missing_headers.rs b/clippy_lints/src/doc/missing_headers.rs index 5a69a5e0d2e8..d62c23a5f13f 100644 --- a/clippy_lints/src/doc/missing_headers.rs +++ b/clippy_lints/src/doc/missing_headers.rs @@ -1,10 +1,12 @@ -use super::{DocHeaders, MISSING_ERRORS_DOC, MISSING_PANICS_DOC, MISSING_SAFETY_DOC, UNNECESSARY_SAFETY_DOC}; +use super::{ + DocHeaders, MISSING_ERRORS_DOC, MISSING_PANICS_DOC, MISSING_SAFETY_DOC, UNNECESSARY_SAFETY_DOC, is_public_api, +}; use clippy_utils::diagnostics::{span_lint, span_lint_and_note}; use clippy_utils::macros::{is_panic, root_macro_call_first_node}; use clippy_utils::res::MaybeDef as _; use clippy_utils::ty::implements_trait_with_env; use clippy_utils::visitors::for_each_expr; -use clippy_utils::{fulfill_or_allowed, is_doc_hidden, is_inside_always_const_context, method_chain_args, return_ty}; +use clippy_utils::{fulfill_or_allowed, is_inside_always_const_context, method_chain_args, return_ty}; use rustc_hir::{BodyId, FnSig, OwnerId, Safety}; use rustc_lint::LateContext; use rustc_middle::ty; @@ -19,17 +21,7 @@ pub fn check( body_id: Option, check_private_items: bool, ) { - if !check_private_items && !cx.effective_visibilities.is_exported(owner_id.def_id) { - return; // Private functions do not require doc comments - } - - // do not lint if any parent has `#[doc(hidden)]` attribute (#7347) - if !check_private_items - && cx - .tcx - .hir_parent_iter(owner_id.into()) - .any(|(id, _node)| is_doc_hidden(cx.tcx.hir_attrs(id))) - { + if !check_private_items && !is_public_api(cx, owner_id) { return; } diff --git a/clippy_lints/src/doc/mod.rs b/clippy_lints/src/doc/mod.rs index 7fde6fcf4dd3..03f2e563f8ef 100644 --- a/clippy_lints/src/doc/mod.rs +++ b/clippy_lints/src/doc/mod.rs @@ -7,7 +7,7 @@ use rustc_ast::token::DocFragmentKind; use rustc_attr_ir::Attribute; use rustc_data_structures::fx::FxHashSet; use rustc_errors::Applicability; -use rustc_hir::{FieldDef, ImplItemKind, ItemKind, Node, Safety, TraitItemKind}; +use rustc_hir::{FieldDef, ImplItemKind, ItemKind, Node, OwnerId, Safety, TraitItemKind}; use rustc_lint::{EarlyContext, EarlyLintPass, LateContext, LateLintPass, LintContext as _, impl_lint_pass}; use rustc_resolve::rustdoc::pulldown_cmark::Event::{ Code, DisplayMath, End, FootnoteReference, HardBreak, Html, InlineHtml, InlineMath, Rule, SoftBreak, Start, @@ -21,12 +21,13 @@ use rustc_resolve::rustdoc::{ DocFragment, add_doc_fragment, attrs_to_doc_fragments, main_body_opts, pulldown_cmark, source_span_for_markdown_range, span_of_fragments, }; -use rustc_span::Span; +use rustc_span::{Ident, Span}; use std::ops::Range; use url::Url; mod broken_link; mod doc_comment_double_space_linebreaks; +mod doc_examples_missing_item; mod doc_paragraphs_missing_punctuation; mod doc_suspicious_footnotes; mod include_in_doc_without_cfg; @@ -100,6 +101,39 @@ declare_clippy_lint! { "double space used for doc comment hard line break instead of `\\`" } +declare_clippy_lint! { + /// ### What it does + /// Checks for functions, constants and statics whose documentation examples never mention + /// the item. A block tagged `ignore` or `compile_fail` is not an example for this purpose. + /// + /// ### Why is this bad? + /// A copied example keeps passing as a doctest while demonstrating another item, leaving + /// the documented item with no example. + /// + /// ### Known problems + /// The check is lexical, so an unrelated identifier spelled like the item satisfies it. + /// + /// ### Example + /// ```no_run + /// /// ``` + /// /// assert_eq!(triple(2), 6); + /// /// ``` + /// pub fn double(x: u32) -> u32 { x * 2 } + /// # pub fn triple(x: u32) -> u32 { x * 3 } + /// ``` + /// Use instead: + /// ```no_run + /// /// ``` + /// /// assert_eq!(double(2), 4); + /// /// ``` + /// pub fn double(x: u32) -> u32 { x * 2 } + /// ``` + #[clippy::version = "1.101.0"] + pub DOC_EXAMPLES_MISSING_ITEM, + pedantic, + "documentation examples never mention the documented item" +} + declare_clippy_lint! { /// ### What it does /// Checks if included files in doc comments are included only for `cfg(doc)`. @@ -709,6 +743,7 @@ declare_clippy_lint! { impl_lint_pass!(Documentation => [ DOC_BROKEN_LINK, DOC_COMMENT_DOUBLE_SPACE_LINEBREAKS, + DOC_EXAMPLES_MISSING_ITEM, DOC_INCLUDE_WITHOUT_CFG, DOC_LAZY_CONTINUATION, DOC_LINK_CODE, @@ -751,7 +786,7 @@ impl EarlyLintPass for Documentation { impl<'tcx> LateLintPass<'tcx> for Documentation { fn check_attributes(&mut self, cx: &LateContext<'tcx>, attrs: &'tcx [Attribute]) { - let Some(headers) = check_attrs(cx, self.valid_idents, attrs) else { + let Some(headers) = check_attrs(cx, self.valid_idents, self.check_private_items, attrs) else { return; }; @@ -858,6 +893,15 @@ struct DocHeaders { first_paragraph_text_len: usize, } +/// Whether the item is part of the documented public API, meaning exported and not under `#[doc(hidden)]` (#7347). +fn is_public_api(cx: &LateContext<'_>, owner_id: OwnerId) -> bool { + cx.effective_visibilities.is_exported(owner_id.def_id) + && !cx + .tcx + .hir_parent_iter(owner_id.into()) + .any(|(id, _)| is_doc_hidden(cx.tcx.hir_attrs(id))) +} + /// Does some pre-processing on raw, desugared `#[doc]` attributes such as parsing them and /// then delegates to `check_doc`. /// Some lints are already checked here if they can work with attributes directly and don't need @@ -865,7 +909,12 @@ struct DocHeaders { /// Others are checked elsewhere, e.g. in `check_doc` if they need access to markdown, or /// back in the various late lint pass methods if they need the final doc headers, like "Safety" or /// "Panics" sections. -fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet, attrs: &[Attribute]) -> Option { +fn check_attrs( + cx: &LateContext<'_>, + valid_idents: &FxHashSet, + check_private_items: bool, + attrs: &[Attribute], +) -> Option { // We don't want the parser to choke on intra doc links. Since we don't // actually care about rendering them, just pretend that all broken links // point to a fake address. @@ -878,6 +927,7 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet, attrs: &[ return None; } + let item_ident = doc_examples_missing_item::item_ident(cx, check_private_items); let (fragments, _) = attrs_to_doc_fragments( attrs.iter().filter_map(|attr| { let (_, kind) = attr.doc_str_and_fragment_kind()?; @@ -966,6 +1016,7 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet, attrs: &[ fragments: &fragments, }, attrs, + item_ident, )) } @@ -1119,6 +1170,7 @@ fn check_doc<'a, Events: Iterator, Range, attrs: &[Attribute], + item_ident: Option, ) -> DocHeaders { // true if a safety header was found let mut headers = DocHeaders::default(); @@ -1133,6 +1185,8 @@ fn check_doc<'a, Events: Iterator, Range = Vec::new(); let mut is_first_paragraph = true; + let mut has_example = false; + let mut example_mentions_item = false; let mut containers = Vec::new(); @@ -1352,6 +1406,12 @@ fn check_doc<'a, Events: Iterator, Range, Range(cx: &LateContext<'tcx>, mut expr: &'a Expr<'b>) -> &'a Expr<'b> { while let Some(init) = expr .res_local_id() @@ -454,6 +455,7 @@ pub fn path_to_local_with_projections(expr: &Expr<'_>) -> Option { /// } /// } /// ``` +#[expect(clippy::doc_examples_missing_item, reason = "the example is the analyzed code")] pub fn trait_ref_of_method<'tcx>(cx: &LateContext<'tcx>, owner: OwnerId) -> Option<&'tcx TraitRef<'tcx>> { if let Node::Item(item) = cx.tcx.hir_node(cx.tcx.hir_owner_parent(owner)) && let ItemKind::Impl(impl_) = &item.kind @@ -749,6 +751,7 @@ fn is_default_equivalent_from(cx: &LateContext<'_>, from_func: &Expr<'_>, arg: & /// /// Note that this check is not recursive, so passing the `if` expression will always return true /// even though sub-expressions might return false. +#[expect(clippy::doc_examples_missing_item, reason = "the example is the analyzed code")] pub fn can_move_expr_to_closure_no_visit<'tcx>( cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, @@ -1423,6 +1426,7 @@ pub fn is_expn_of(mut span: Span, name: Symbol) -> Option { /// ``` /// `42` is considered expanded from `foo!` and `bar!` by `is_expn_of` but only /// from `bar!` by `is_direct_expn_of`. +#[expect(clippy::doc_examples_missing_item, reason = "the example is the analyzed code")] #[must_use] pub fn is_direct_expn_of(span: Span, name: Symbol) -> Option { if span.from_expansion() { @@ -2086,6 +2090,7 @@ pub fn is_no_core_crate(cx: &LateContext<'_>) -> bool { /// fn f() {} /// } /// ``` +#[expect(clippy::doc_examples_missing_item, reason = "the example is the analyzed code")] pub fn is_trait_impl_item(cx: &LateContext<'_>, hir_id: HirId) -> bool { if let Node::Item(item) = cx.tcx.parent_hir_node(hir_id) { matches!(item.kind, ItemKind::Impl(Impl { of_trait: Some(_), .. })) diff --git a/tests/ui-toml/private-doc-errors/doc_lints.rs b/tests/ui-toml/private-doc-errors/doc_lints.rs index 4d354e22c6d2..9fd0e0990dde 100644 --- a/tests/ui-toml/private-doc-errors/doc_lints.rs +++ b/tests/ui-toml/private-doc-errors/doc_lints.rs @@ -1,4 +1,5 @@ #![deny( + clippy::doc_examples_missing_item, clippy::missing_errors_doc, clippy::missing_panics_doc, clippy::unnecessary_safety_doc @@ -51,6 +52,28 @@ pub mod __macro { } } +mod examples { + /// ``` + /// let _ = 1; + /// ``` + fn private_copied() {} + //~^ doc_examples_missing_item + + /// ``` + /// private_own(); + /// ``` + fn private_own() {} +} + +#[doc(hidden)] +pub mod hidden_examples { + /// ``` + /// let _ = 1; + /// ``` + pub fn under_hidden() {} + //~^ doc_examples_missing_item +} + #[warn(clippy::missing_errors_doc)] #[test] fn test() -> Result<(), ()> { diff --git a/tests/ui-toml/private-doc-errors/doc_lints.stderr b/tests/ui-toml/private-doc-errors/doc_lints.stderr index d83e10ddb94e..e8aa35c936e7 100644 --- a/tests/ui-toml/private-doc-errors/doc_lints.stderr +++ b/tests/ui-toml/private-doc-errors/doc_lints.stderr @@ -1,58 +1,58 @@ error: safe function's docs have unnecessary `# Safety` section - --> tests/ui-toml/private-doc-errors/doc_lints.rs:12:1 + --> tests/ui-toml/private-doc-errors/doc_lints.rs:13:1 | LL | fn you_dont_see_me() { | ^^^^^^^^^^^^^^^^^^^^ | note: the lint level is defined here - --> tests/ui-toml/private-doc-errors/doc_lints.rs:4:5 + --> tests/ui-toml/private-doc-errors/doc_lints.rs:5:5 | LL | clippy::unnecessary_safety_doc | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: safe function's docs have unnecessary `# Safety` section - --> tests/ui-toml/private-doc-errors/doc_lints.rs:23:5 + --> tests/ui-toml/private-doc-errors/doc_lints.rs:24:5 | LL | pub fn only_crate_wide_accessible() -> Result<(), ()> { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: docs for function returning `Result` missing `# Errors` section - --> tests/ui-toml/private-doc-errors/doc_lints.rs:23:5 + --> tests/ui-toml/private-doc-errors/doc_lints.rs:24:5 | LL | pub fn only_crate_wide_accessible() -> Result<(), ()> { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | note: the lint level is defined here - --> tests/ui-toml/private-doc-errors/doc_lints.rs:2:5 + --> tests/ui-toml/private-doc-errors/doc_lints.rs:3:5 | LL | clippy::missing_errors_doc, | ^^^^^^^^^^^^^^^^^^^^^^^^^^ error: safe function's docs have unnecessary `# Safety` section - --> tests/ui-toml/private-doc-errors/doc_lints.rs:38:5 + --> tests/ui-toml/private-doc-errors/doc_lints.rs:39:5 | LL | fn private(&self) { | ^^^^^^^^^^^^^^^^^ error: docs for function which may panic missing `# Panics` section - --> tests/ui-toml/private-doc-errors/doc_lints.rs:38:5 + --> tests/ui-toml/private-doc-errors/doc_lints.rs:39:5 | LL | fn private(&self) { | ^^^^^^^^^^^^^^^^^ | note: first possible panic found here - --> tests/ui-toml/private-doc-errors/doc_lints.rs:41:9 + --> tests/ui-toml/private-doc-errors/doc_lints.rs:42:9 | LL | panic!(); | ^^^^^^^^ note: the lint level is defined here - --> tests/ui-toml/private-doc-errors/doc_lints.rs:3:5 + --> tests/ui-toml/private-doc-errors/doc_lints.rs:4:5 | LL | clippy::missing_panics_doc, | ^^^^^^^^^^^^^^^^^^^^^^^^^^ error: unsafe function's docs are missing a `# Safety` section - --> tests/ui-toml/private-doc-errors/doc_lints.rs:49:9 + --> tests/ui-toml/private-doc-errors/doc_lints.rs:50:9 | LL | pub unsafe fn f() {} | ^^^^^^^^^^^^^^^^^ @@ -60,5 +60,26 @@ LL | pub unsafe fn f() {} = note: `-D clippy::missing-safety-doc` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(clippy::missing_safety_doc)]` -error: aborting due to 6 previous errors +error: none of the documentation examples mention `private_copied` + --> tests/ui-toml/private-doc-errors/doc_lints.rs:59:5 + | +LL | fn private_copied() {} + | ^^^^^^^^^^^^^^^^^^^ + | + = help: consider adding an example that uses this item +note: the lint level is defined here + --> tests/ui-toml/private-doc-errors/doc_lints.rs:2:5 + | +LL | clippy::doc_examples_missing_item, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: none of the documentation examples mention `under_hidden` + --> tests/ui-toml/private-doc-errors/doc_lints.rs:73:5 + | +LL | pub fn under_hidden() {} + | ^^^^^^^^^^^^^^^^^^^^^ + | + = help: consider adding an example that uses this item + +error: aborting due to 8 previous errors diff --git a/tests/ui/doc_examples_missing_item/doc_examples_missing_item.rs b/tests/ui/doc_examples_missing_item/doc_examples_missing_item.rs new file mode 100644 index 000000000000..6b12dfc300c1 --- /dev/null +++ b/tests/ui/doc_examples_missing_item/doc_examples_missing_item.rs @@ -0,0 +1,234 @@ +#![warn(clippy::doc_examples_missing_item)] + +//! Module documentation is not checked. +//! ``` +//! let _ = 1; +//! ``` + +/// ``` +/// assert_eq!(triple(2), 6); +/// ``` +pub fn double(x: u32) -> u32 { + //~^ doc_examples_missing_item + x * 2 +} + +/// ``` +/// assert_eq!(triple(2), 6); +/// ``` +pub fn triple(x: u32) -> u32 { + x * 3 +} + +/// Documentation without an example. +pub fn no_example() {} + +unsafe extern "C" { + /// A foreign declaration has a documentation page of its own. + /// ``` + /// let _ = 1; + /// ``` + pub fn readv(fd: i32) -> isize; + //~^ doc_examples_missing_item +} + +/// Tagged `ignore` and `compile_fail` blocks are not examples. +/// ```text +/// text_block(); +/// ``` +/// ```ignore +/// text_block(); +/// ``` +/// ```compile_fail +/// let _: u8 = text_block; +/// ``` +/// ``` +/// let _ = 1; +/// ``` +pub fn text_block() {} +//~^ doc_examples_missing_item + +/// ```no_run +/// let _ = 1; +/// ``` +pub fn no_run_block() {} +//~^ doc_examples_missing_item + +/// A conditional `ignore-*` stays an example on other targets. +/// ```ignore-windows +/// let _ = 1; +/// ``` +pub fn conditional_ignore() {} +//~^ doc_examples_missing_item + +/// Hidden lines are compiled, so they count. +/// ``` +/// # hidden_line(); +/// let _ = 1; +/// ``` +pub fn hidden_line() {} + +/// One mention anywhere in the examples is enough. +/// ``` +/// let _ = 1; +/// ``` +/// ``` +/// later_block(); +/// ``` +pub fn later_block() {} + +/// ``` +/// let _ = 1; +/// ``` +/// ``` +/// let _ = 2; +/// ``` +pub fn every_block_misses() {} +//~^ doc_examples_missing_item + +/// ``` +/// let _ = r#type(1); +/// ``` +pub fn r#type(x: u32) -> u32 { + x +} + +/// ``` +/// let _ = 1; +/// ``` +pub static VERSION: &str = "1"; +//~^ doc_examples_missing_item + +/// ``` +/// let _ = 1; +/// ``` +pub const MAX: u32 = 8; +//~^ doc_examples_missing_item + +/// Carries the methods below. +pub struct Counter { + pub ticks: u32, +} + +impl Counter { + /// ``` + /// let _ = 1; + /// ``` + pub fn reset(&mut self) { + //~^ doc_examples_missing_item + self.ticks = 0; + } + + /// ``` + /// let _ = 1; + /// ``` + pub const LIMIT: u32 = 8; + //~^ doc_examples_missing_item +} + +/// Carries the required method below. +pub trait Shape { + /// ``` + /// let _ = 1; + /// ``` + fn area(&self) -> u32; + //~^ doc_examples_missing_item +} + +impl Shape for Counter { + /// Trait impls show the trait's documentation. + /// ``` + /// let _ = 1; + /// ``` + fn area(&self) -> u32 { + self.ticks + } +} + +mod private { + /// ``` + /// let _ = 1; + /// ``` + pub fn not_exported() {} + + /// ``` + /// let _ = 1; + /// ``` + pub fn reexported() {} + //~^ doc_examples_missing_item +} + +pub use private::reexported; + +#[doc(hidden)] +pub mod hidden { + /// ``` + /// let _ = 1; + /// ``` + pub fn under_hidden() {} +} + +#[expect(clippy::doc_examples_missing_item)] +/// ``` +/// let _ = 1; +/// ``` +pub fn allowed() {} + +macro_rules! shared_example { + ($($name:ident),*) => { + $( + /// ``` + /// generated_first(); + /// ``` + pub fn $name() {} + //~^ doc_examples_missing_item + )* + }; +} + +shared_example!(generated_first, generated_second); + +macro_rules! named_example { + ($($name:ident),*) => { + $( + #[doc = concat!("```\n", stringify!($name), "();\n```")] + pub fn $name() {} + )* + }; +} + +named_example!(named_first, named_second); + +// Types and traits are skipped because examples often use them through a glob import without naming them. +pub mod unchecked_kinds { + /// ``` + /// let _ = 1; + /// ``` + pub struct Counter { + pub ticks: u32, + } + + /// ``` + /// let _ = 1; + /// ``` + pub enum Direction { + Up, + } + + /// ``` + /// let _ = 1; + /// ``` + pub union Raw { + bits: u32, + } + + /// ``` + /// let _ = 1; + /// ``` + pub trait Shape {} + + /// ``` + /// let _ = 1; + /// ``` + pub type Bytes = Vec; +} diff --git a/tests/ui/doc_examples_missing_item/doc_examples_missing_item.stderr b/tests/ui/doc_examples_missing_item/doc_examples_missing_item.stderr new file mode 100644 index 000000000000..415e7b50e3f3 --- /dev/null +++ b/tests/ui/doc_examples_missing_item/doc_examples_missing_item.stderr @@ -0,0 +1,113 @@ +error: none of the documentation examples mention `double` + --> tests/ui/doc_examples_missing_item/doc_examples_missing_item.rs:11:1 + | +LL | pub fn double(x: u32) -> u32 { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: consider adding an example that uses this item + = note: `-D clippy::doc-examples-missing-item` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::doc_examples_missing_item)]` + +error: none of the documentation examples mention `readv` + --> tests/ui/doc_examples_missing_item/doc_examples_missing_item.rs:31:5 + | +LL | pub fn readv(fd: i32) -> isize; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: consider adding an example that uses this item + +error: none of the documentation examples mention `text_block` + --> tests/ui/doc_examples_missing_item/doc_examples_missing_item.rs:48:1 + | +LL | pub fn text_block() {} + | ^^^^^^^^^^^^^^^^^^^ + | + = help: consider adding an example that uses this item + +error: none of the documentation examples mention `no_run_block` + --> tests/ui/doc_examples_missing_item/doc_examples_missing_item.rs:54:1 + | +LL | pub fn no_run_block() {} + | ^^^^^^^^^^^^^^^^^^^^^ + | + = help: consider adding an example that uses this item + +error: none of the documentation examples mention `conditional_ignore` + --> tests/ui/doc_examples_missing_item/doc_examples_missing_item.rs:61:1 + | +LL | pub fn conditional_ignore() {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: consider adding an example that uses this item + +error: none of the documentation examples mention `every_block_misses` + --> tests/ui/doc_examples_missing_item/doc_examples_missing_item.rs:86:1 + | +LL | pub fn every_block_misses() {} + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: consider adding an example that uses this item + +error: none of the documentation examples mention `VERSION` + --> tests/ui/doc_examples_missing_item/doc_examples_missing_item.rs:99:1 + | +LL | pub static VERSION: &str = "1"; + | ^^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: consider adding an example that uses this item + +error: none of the documentation examples mention `MAX` + --> tests/ui/doc_examples_missing_item/doc_examples_missing_item.rs:105:1 + | +LL | pub const MAX: u32 = 8; + | ^^^^^^^^^^^^^^^^^^ + | + = help: consider adding an example that uses this item + +error: none of the documentation examples mention `reset` + --> tests/ui/doc_examples_missing_item/doc_examples_missing_item.rs:117:5 + | +LL | pub fn reset(&mut self) { + | ^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: consider adding an example that uses this item + +error: none of the documentation examples mention `LIMIT` + --> tests/ui/doc_examples_missing_item/doc_examples_missing_item.rs:125:5 + | +LL | pub const LIMIT: u32 = 8; + | ^^^^^^^^^^^^^^^^^^^^ + | + = help: consider adding an example that uses this item + +error: none of the documentation examples mention `area` + --> tests/ui/doc_examples_missing_item/doc_examples_missing_item.rs:134:5 + | +LL | fn area(&self) -> u32; + | ^^^^^^^^^^^^^^^^^^^^^^ + | + = help: consider adding an example that uses this item + +error: none of the documentation examples mention `reexported` + --> tests/ui/doc_examples_missing_item/doc_examples_missing_item.rs:157:5 + | +LL | pub fn reexported() {} + | ^^^^^^^^^^^^^^^^^^^ + | + = help: consider adding an example that uses this item + +error: none of the documentation examples mention `generated_second` + --> tests/ui/doc_examples_missing_item/doc_examples_missing_item.rs:183:13 + | +LL | macro_rules! shared_example { +... +LL | pub fn $name() {} + | ^^^^^^^^^^^^^^ +... +LL | shared_example!(generated_first, generated_second); + | -------------------------------------------------- in this macro invocation + | + = help: consider adding an example that uses this item + +error: aborting due to 13 previous errors + diff --git a/tests/ui/doc_examples_missing_item/doc_examples_missing_item_proc_macro.rs b/tests/ui/doc_examples_missing_item/doc_examples_missing_item_proc_macro.rs new file mode 100644 index 000000000000..deea0373975c --- /dev/null +++ b/tests/ui/doc_examples_missing_item/doc_examples_missing_item_proc_macro.rs @@ -0,0 +1,14 @@ +//@check-pass +#![warn(clippy::doc_examples_missing_item)] + +extern crate proc_macro; + +use proc_macro::TokenStream; + +/// ``` +/// let _ = 1; +/// ``` +#[proc_macro_derive(MyDerivedTrait)] +pub fn myderive(t: TokenStream) -> TokenStream { + t +}