diff --git a/CHANGELOG.md b/CHANGELOG.md index c773cf15756b..f19526a7fa49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6066,6 +6066,7 @@ Released 2018-09-13 [`diverging_sub_expression`]: https://rust-lang.github.io/rust-clippy/master/index.html#diverging_sub_expression [`doc_broken_link`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_broken_link [`doc_comment_double_space_linebreaks`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_comment_double_space_linebreaks +[`doc_comments_missing_terminal_punctuation`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_comments_missing_terminal_punctuation [`doc_include_without_cfg`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_include_without_cfg [`doc_lazy_continuation`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_lazy_continuation [`doc_link_code`]: https://rust-lang.github.io/rust-clippy/master/index.html#doc_link_code diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index 5563b8094f01..9de1496afb57 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -114,6 +114,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::disallowed_script_idents::DISALLOWED_SCRIPT_IDENTS_INFO, crate::disallowed_types::DISALLOWED_TYPES_INFO, crate::doc::DOC_BROKEN_LINK_INFO, + crate::doc::DOC_COMMENTS_MISSING_TERMINAL_PUNCTUATION_INFO, crate::doc::DOC_COMMENT_DOUBLE_SPACE_LINEBREAKS_INFO, crate::doc::DOC_INCLUDE_WITHOUT_CFG_INFO, crate::doc::DOC_LAZY_CONTINUATION_INFO, diff --git a/clippy_lints/src/doc/doc_comments_missing_terminal_punctuation.rs b/clippy_lints/src/doc/doc_comments_missing_terminal_punctuation.rs new file mode 100644 index 000000000000..46e2550431f6 --- /dev/null +++ b/clippy_lints/src/doc/doc_comments_missing_terminal_punctuation.rs @@ -0,0 +1,117 @@ +use rustc_errors::Applicability; +use rustc_lint::LateContext; +use rustc_resolve::rustdoc::main_body_opts; + +use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd}; + +use super::{DOC_COMMENTS_MISSING_TERMINAL_PUNCTUATION, Fragments}; + +const MSG: &str = "doc comments should end with a terminal punctuation mark"; +const PUNCTUATION_SUGGESTION: char = '.'; + +pub fn check(cx: &LateContext<'_>, doc: &str, fragments: Fragments<'_>) { + match is_missing_punctuation(doc) { + IsMissingPunctuation::Fixable(offset) => { + // This ignores `#[doc]` attributes, which we do not handle. + if let Some(span) = fragments.span(cx, offset..offset) { + clippy_utils::diagnostics::span_lint_and_sugg( + cx, + DOC_COMMENTS_MISSING_TERMINAL_PUNCTUATION, + span, + MSG, + "end the doc comment with some punctuation", + PUNCTUATION_SUGGESTION.to_string(), + Applicability::MaybeIncorrect, + ); + } + }, + IsMissingPunctuation::Unfixable(offset) => { + // This ignores `#[doc]` attributes, which we do not handle. + if let Some(span) = fragments.span(cx, offset..offset) { + clippy_utils::diagnostics::span_lint_and_help( + cx, + DOC_COMMENTS_MISSING_TERMINAL_PUNCTUATION, + span, + MSG, + None, + "end the doc comment with some punctuation", + ); + } + }, + IsMissingPunctuation::No => {}, + } +} + +#[must_use] +/// If punctuation is missing, returns the offset where new punctuation should be inserted. +fn is_missing_punctuation(doc_string: &str) -> IsMissingPunctuation { + const TERMINAL_PUNCTUATION_MARKS: &[char] = &['.', '?', '!', '…']; + + let mut no_report_depth = 0; + let mut missing_punctuation = IsMissingPunctuation::No; + for (event, offset) in + Parser::new_ext(doc_string, main_body_opts() - Options::ENABLE_SMART_PUNCTUATION).into_offset_iter() + { + match event { + Event::Start( + Tag::CodeBlock(..) + | Tag::FootnoteDefinition(_) + | Tag::Heading { .. } + | Tag::HtmlBlock + | Tag::List(..) + | Tag::Table(_), + ) => { + no_report_depth += 1; + }, + Event::End(TagEnd::FootnoteDefinition) => { + no_report_depth -= 1; + }, + Event::End( + TagEnd::CodeBlock | TagEnd::Heading(_) | TagEnd::HtmlBlock | TagEnd::List(_) | TagEnd::Table, + ) => { + no_report_depth -= 1; + missing_punctuation = IsMissingPunctuation::No; + }, + Event::InlineHtml(_) | Event::Start(Tag::Image { .. }) | Event::End(TagEnd::Image) => { + missing_punctuation = IsMissingPunctuation::No; + }, + Event::Code(..) | Event::Start(Tag::Link { .. }) | Event::End(TagEnd::Link) + if no_report_depth == 0 && !offset.is_empty() => + { + if doc_string[..offset.end] + .trim_end() + .ends_with(TERMINAL_PUNCTUATION_MARKS) + { + missing_punctuation = IsMissingPunctuation::No; + } else { + missing_punctuation = IsMissingPunctuation::Fixable(offset.end); + } + }, + Event::Text(..) if no_report_depth == 0 && !offset.is_empty() => { + let trimmed = doc_string[..offset.end].trim_end(); + if trimmed.ends_with(TERMINAL_PUNCTUATION_MARKS) { + missing_punctuation = IsMissingPunctuation::No; + } else if let Some(t) = trimmed.strip_suffix(|c| c == ')' || c == '"') { + if t.ends_with(TERMINAL_PUNCTUATION_MARKS) { + // Avoid false positives. + missing_punctuation = IsMissingPunctuation::No; + } else { + missing_punctuation = IsMissingPunctuation::Unfixable(offset.end); + } + } else { + missing_punctuation = IsMissingPunctuation::Fixable(offset.end); + } + }, + _ => {}, + } + } + + missing_punctuation +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum IsMissingPunctuation { + Fixable(usize), + Unfixable(usize), + No, +} diff --git a/clippy_lints/src/doc/mod.rs b/clippy_lints/src/doc/mod.rs index f8ae770b3a4d..ec2b999b5012 100644 --- a/clippy_lints/src/doc/mod.rs +++ b/clippy_lints/src/doc/mod.rs @@ -26,6 +26,7 @@ use url::Url; mod broken_link; mod doc_comment_double_space_linebreaks; +mod doc_comments_missing_terminal_punctuation; mod doc_suspicious_footnotes; mod include_in_doc_without_cfg; mod lazy_continuation; @@ -668,6 +669,28 @@ declare_clippy_lint! { "looks like a link or footnote ref, but with no definition" } +declare_clippy_lint! { + /// ### What it does + /// Checks for doc comments that do not end with a period or another punctuation mark. + /// Various Markdowns constructs are taken into account to avoid false positives. + /// + /// ### Why is this bad? + /// A project may wish to enforce consistent doc comments by making sure they end with a punctuation mark. + /// + /// ### Example + /// ```no_run + /// /// Returns the Answer to the Ultimate Question of Life, the Universe, and Everything + /// ``` + /// Use instead: + /// ```no_run + /// /// Returns the Answer to the Ultimate Question of Life, the Universe, and Everything. + /// ``` + #[clippy::version = "1.92.0"] + pub DOC_COMMENTS_MISSING_TERMINAL_PUNCTUATION, + nursery, + "missing terminal punctuation in doc comments" +} + pub struct Documentation { valid_idents: FxHashSet, check_private_items: bool, @@ -702,6 +725,7 @@ impl_lint_pass!(Documentation => [ DOC_INCLUDE_WITHOUT_CFG, DOC_COMMENT_DOUBLE_SPACE_LINEBREAKS, DOC_SUSPICIOUS_FOOTNOTES, + DOC_COMMENTS_MISSING_TERMINAL_PUNCTUATION, ]); impl EarlyLintPass for Documentation { @@ -873,6 +897,15 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet, attrs: &[ }, ); + doc_comments_missing_terminal_punctuation::check( + cx, + &doc, + Fragments { + doc: &doc, + fragments: &fragments, + }, + ); + // NOTE: check_doc uses it own cb function, // to avoid causing duplicated diagnostics for the broken link checker. let mut full_fake_broken_link_callback = |bl: BrokenLink<'_>| -> Option<(CowStr<'_>, CowStr<'_>)> { diff --git a/tests/ui/doc/doc_comments_missing_terminal_punctuation.fixed b/tests/ui/doc/doc_comments_missing_terminal_punctuation.fixed new file mode 100644 index 000000000000..a1f1da33d806 --- /dev/null +++ b/tests/ui/doc/doc_comments_missing_terminal_punctuation.fixed @@ -0,0 +1,166 @@ +#![feature(custom_inner_attributes)] +#![rustfmt::skip] +#![warn(clippy::doc_comments_missing_terminal_punctuation)] + +/// Returns the Answer to the Ultimate Question of Life, the Universe, and Everything. +//~^ doc_comments_missing_terminal_punctuation +fn answer() -> i32 { + 42 +} + +/// The `Option` type. +//~^ doc_comments_missing_terminal_punctuation +// Triggers even in the presence of another attribute. +#[derive(Debug)] +enum MyOption { + /// No value. + //~^ doc_comments_missing_terminal_punctuation + None, + /// Some value of type `T`. + Some(T), +} + +// Triggers correctly even when interleaved with other attributes. +/// A multiline +#[derive(Debug)] +/// doc comment: +/// only the last line triggers the lint. +//~^ doc_comments_missing_terminal_punctuation +enum Exceptions { + /// Question marks are fine? + QuestionMark, + /// Exclamation marks are fine! + ExclamationMark, + /// Ellipses are ok too… + Ellipsis, + /// HTML content is however not checked: + /// Raw HTML is allowed as well + RawHtml, + /// The raw HTML exception actually does the right thing to autolinks: + /// . + //~^ doc_comments_missing_terminal_punctuation + MarkdownAutolink, + /// This table introduction ends with a colon: + /// + /// | Exception | Note | + /// | -------------- | ----- | + /// | Markdown table | A-ok | + MarkdownTable, + /// Here is a snippet + /// + /// ``` + /// // Code blocks are no issues. + /// ``` + CodeBlock, +} + +// Check the lint can be expected on a whole enum at once. +#[expect(clippy::doc_comments_missing_terminal_punctuation)] +enum Char { + /// U+0000 + Null, + /// U+0001 + StartOfHeading, +} + +// Check the lint can be expected on a single variant without affecting others. +enum Char2 { + #[expect(clippy::doc_comments_missing_terminal_punctuation)] + /// U+0000 + Null, + /// U+0001. + //~^ doc_comments_missing_terminal_punctuation + StartOfHeading, +} + +mod module { + //! Works on + //! inner attributes too. + //~^ doc_comments_missing_terminal_punctuation +} + +enum Trailers { + /// Sometimes the last sentence ends with parentheses (and that's ok). + ParensPassing, + /// (Sometimes the last sentence is in parentheses.) + SentenceInParensPassing, + /// **Sometimes the last sentence is in bold, and that's ok.** + DoubleStarPassing, + /// **But sometimes it is missing a period.** + //~^ doc_comments_missing_terminal_punctuation + DoubleStarFailing, + /// _Sometimes the last sentence is in italics, and that's ok._ + UnderscorePassing, + /// _But sometimes it is missing a period._ + //~^ doc_comments_missing_terminal_punctuation + UnderscoreFailing, + /// This comment ends with "a quote." + AmericanStyleQuotePassing, + /// This comment ends with "a quote". + BritishStyleQuotePassing, +} + +/// Doc comments can end with an [inline link](#anchor). +//~^ doc_comments_missing_terminal_punctuation +struct InlineLink; + +/// Some doc comments contain [link reference definitions][spec]. +//~^ doc_comments_missing_terminal_punctuation +/// +/// [spec]: https://spec.commonmark.org/0.31.2/#link-reference-definitions +struct LinkRefDefinition; + +// List items do not always need to end with a period. +enum UnorderedLists { + /// This list has an introductory sentence: + /// + /// - A list item + Dash, + /// + A list item + Plus, + /// * A list item + Star, +} + +enum OrderedLists { + /// 1. A list item + Dot, + /// 42) A list item + Paren, +} + +/// Doc comments with trailing blank lines are supported. +//~^ doc_comments_missing_terminal_punctuation +/// +struct TrailingBlankLine; + +/// The first paragraph is not checked +/// +/// Other sentences are not either +/// Only the last sentence is. +//~^ doc_comments_missing_terminal_punctuation +struct OnlyLastSentence; + +/// ``` +struct IncompleteBlockCode; + +/// This ends with a code `span`. +//~^ doc_comments_missing_terminal_punctuation +struct CodeSpan; + +#[expect(clippy::empty_docs)] +/// +struct EmptyDocComment; + +/** + * Block doc comments work. + * + */ +//~^^^ doc_comments_missing_terminal_punctuation +struct BlockDocComment; + +/// Sometimes a doc attribute is used for concatenation +/// ``` +#[doc = ""] +/// ``` +struct DocAttribute; diff --git a/tests/ui/doc/doc_comments_missing_terminal_punctuation.rs b/tests/ui/doc/doc_comments_missing_terminal_punctuation.rs new file mode 100644 index 000000000000..85c1b31dcd5b --- /dev/null +++ b/tests/ui/doc/doc_comments_missing_terminal_punctuation.rs @@ -0,0 +1,166 @@ +#![feature(custom_inner_attributes)] +#![rustfmt::skip] +#![warn(clippy::doc_comments_missing_terminal_punctuation)] + +/// Returns the Answer to the Ultimate Question of Life, the Universe, and Everything +//~^ doc_comments_missing_terminal_punctuation +fn answer() -> i32 { + 42 +} + +/// The `Option` type +//~^ doc_comments_missing_terminal_punctuation +// Triggers even in the presence of another attribute. +#[derive(Debug)] +enum MyOption { + /// No value + //~^ doc_comments_missing_terminal_punctuation + None, + /// Some value of type `T`. + Some(T), +} + +// Triggers correctly even when interleaved with other attributes. +/// A multiline +#[derive(Debug)] +/// doc comment: +/// only the last line triggers the lint +//~^ doc_comments_missing_terminal_punctuation +enum Exceptions { + /// Question marks are fine? + QuestionMark, + /// Exclamation marks are fine! + ExclamationMark, + /// Ellipses are ok too… + Ellipsis, + /// HTML content is however not checked: + /// Raw HTML is allowed as well + RawHtml, + /// The raw HTML exception actually does the right thing to autolinks: + /// + //~^ doc_comments_missing_terminal_punctuation + MarkdownAutolink, + /// This table introduction ends with a colon: + /// + /// | Exception | Note | + /// | -------------- | ----- | + /// | Markdown table | A-ok | + MarkdownTable, + /// Here is a snippet + /// + /// ``` + /// // Code blocks are no issues. + /// ``` + CodeBlock, +} + +// Check the lint can be expected on a whole enum at once. +#[expect(clippy::doc_comments_missing_terminal_punctuation)] +enum Char { + /// U+0000 + Null, + /// U+0001 + StartOfHeading, +} + +// Check the lint can be expected on a single variant without affecting others. +enum Char2 { + #[expect(clippy::doc_comments_missing_terminal_punctuation)] + /// U+0000 + Null, + /// U+0001 + //~^ doc_comments_missing_terminal_punctuation + StartOfHeading, +} + +mod module { + //! Works on + //! inner attributes too + //~^ doc_comments_missing_terminal_punctuation +} + +enum Trailers { + /// Sometimes the last sentence ends with parentheses (and that's ok). + ParensPassing, + /// (Sometimes the last sentence is in parentheses.) + SentenceInParensPassing, + /// **Sometimes the last sentence is in bold, and that's ok.** + DoubleStarPassing, + /// **But sometimes it is missing a period** + //~^ doc_comments_missing_terminal_punctuation + DoubleStarFailing, + /// _Sometimes the last sentence is in italics, and that's ok._ + UnderscorePassing, + /// _But sometimes it is missing a period_ + //~^ doc_comments_missing_terminal_punctuation + UnderscoreFailing, + /// This comment ends with "a quote." + AmericanStyleQuotePassing, + /// This comment ends with "a quote". + BritishStyleQuotePassing, +} + +/// Doc comments can end with an [inline link](#anchor) +//~^ doc_comments_missing_terminal_punctuation +struct InlineLink; + +/// Some doc comments contain [link reference definitions][spec] +//~^ doc_comments_missing_terminal_punctuation +/// +/// [spec]: https://spec.commonmark.org/0.31.2/#link-reference-definitions +struct LinkRefDefinition; + +// List items do not always need to end with a period. +enum UnorderedLists { + /// This list has an introductory sentence: + /// + /// - A list item + Dash, + /// + A list item + Plus, + /// * A list item + Star, +} + +enum OrderedLists { + /// 1. A list item + Dot, + /// 42) A list item + Paren, +} + +/// Doc comments with trailing blank lines are supported +//~^ doc_comments_missing_terminal_punctuation +/// +struct TrailingBlankLine; + +/// The first paragraph is not checked +/// +/// Other sentences are not either +/// Only the last sentence is +//~^ doc_comments_missing_terminal_punctuation +struct OnlyLastSentence; + +/// ``` +struct IncompleteBlockCode; + +/// This ends with a code `span` +//~^ doc_comments_missing_terminal_punctuation +struct CodeSpan; + +#[expect(clippy::empty_docs)] +/// +struct EmptyDocComment; + +/** + * Block doc comments work + * + */ +//~^^^ doc_comments_missing_terminal_punctuation +struct BlockDocComment; + +/// Sometimes a doc attribute is used for concatenation +/// ``` +#[doc = ""] +/// ``` +struct DocAttribute; diff --git a/tests/ui/doc/doc_comments_missing_terminal_punctuation.stderr b/tests/ui/doc/doc_comments_missing_terminal_punctuation.stderr new file mode 100644 index 000000000000..bd36e7ace929 --- /dev/null +++ b/tests/ui/doc/doc_comments_missing_terminal_punctuation.stderr @@ -0,0 +1,95 @@ +error: doc comments should end with a terminal punctuation mark + --> tests/ui/doc/doc_comments_missing_terminal_punctuation.rs:5:86 + | +LL | /// Returns the Answer to the Ultimate Question of Life, the Universe, and Everything + | ^ help: end the doc comment with some punctuation: `.` + | + = note: `-D clippy::doc-comments-missing-terminal-punctuation` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::doc_comments_missing_terminal_punctuation)]` + +error: doc comments should end with a terminal punctuation mark + --> tests/ui/doc/doc_comments_missing_terminal_punctuation.rs:11:22 + | +LL | /// The `Option` type + | ^ help: end the doc comment with some punctuation: `.` + +error: doc comments should end with a terminal punctuation mark + --> tests/ui/doc/doc_comments_missing_terminal_punctuation.rs:16:17 + | +LL | /// No value + | ^ help: end the doc comment with some punctuation: `.` + +error: doc comments should end with a terminal punctuation mark + --> tests/ui/doc/doc_comments_missing_terminal_punctuation.rs:27:41 + | +LL | /// only the last line triggers the lint + | ^ help: end the doc comment with some punctuation: `.` + +error: doc comments should end with a terminal punctuation mark + --> tests/ui/doc/doc_comments_missing_terminal_punctuation.rs:40:56 + | +LL | /// + | ^ help: end the doc comment with some punctuation: `.` + +error: doc comments should end with a terminal punctuation mark + --> tests/ui/doc/doc_comments_missing_terminal_punctuation.rs:71:15 + | +LL | /// U+0001 + | ^ help: end the doc comment with some punctuation: `.` + +error: doc comments should end with a terminal punctuation mark + --> tests/ui/doc/doc_comments_missing_terminal_punctuation.rs:78:29 + | +LL | //! inner attributes too + | ^ help: end the doc comment with some punctuation: `.` + +error: doc comments should end with a terminal punctuation mark + --> tests/ui/doc/doc_comments_missing_terminal_punctuation.rs:89:47 + | +LL | /// **But sometimes it is missing a period** + | ^ help: end the doc comment with some punctuation: `.` + +error: doc comments should end with a terminal punctuation mark + --> tests/ui/doc/doc_comments_missing_terminal_punctuation.rs:94:46 + | +LL | /// _But sometimes it is missing a period_ + | ^ help: end the doc comment with some punctuation: `.` + +error: doc comments should end with a terminal punctuation mark + --> tests/ui/doc/doc_comments_missing_terminal_punctuation.rs:103:56 + | +LL | /// Doc comments can end with an [inline link](#anchor) + | ^ help: end the doc comment with some punctuation: `.` + +error: doc comments should end with a terminal punctuation mark + --> tests/ui/doc/doc_comments_missing_terminal_punctuation.rs:107:65 + | +LL | /// Some doc comments contain [link reference definitions][spec] + | ^ help: end the doc comment with some punctuation: `.` + +error: doc comments should end with a terminal punctuation mark + --> tests/ui/doc/doc_comments_missing_terminal_punctuation.rs:132:57 + | +LL | /// Doc comments with trailing blank lines are supported + | ^ help: end the doc comment with some punctuation: `.` + +error: doc comments should end with a terminal punctuation mark + --> tests/ui/doc/doc_comments_missing_terminal_punctuation.rs:140:30 + | +LL | /// Only the last sentence is + | ^ help: end the doc comment with some punctuation: `.` + +error: doc comments should end with a terminal punctuation mark + --> tests/ui/doc/doc_comments_missing_terminal_punctuation.rs:147:33 + | +LL | /// This ends with a code `span` + | ^ help: end the doc comment with some punctuation: `.` + +error: doc comments should end with a terminal punctuation mark + --> tests/ui/doc/doc_comments_missing_terminal_punctuation.rs:156:27 + | +LL | * Block doc comments work + | ^ help: end the doc comment with some punctuation: `.` + +error: aborting due to 15 previous errors + diff --git a/tests/ui/doc/doc_comments_missing_terminal_punctuation_unfixable.rs b/tests/ui/doc/doc_comments_missing_terminal_punctuation_unfixable.rs new file mode 100644 index 000000000000..1c52441eb656 --- /dev/null +++ b/tests/ui/doc/doc_comments_missing_terminal_punctuation_unfixable.rs @@ -0,0 +1,13 @@ +#![feature(custom_inner_attributes)] +#![rustfmt::skip] +#![warn(clippy::doc_comments_missing_terminal_punctuation)] +//@no-rustfix + +enum UnfixableTrailers { + /// Sometimes the doc comment ends with parentheses (like this) + //~^ doc_comments_missing_terminal_punctuation + EndsWithParens, + /// This comment ends with "a quote" + //~^ doc_comments_missing_terminal_punctuation + QuoteFailing, +} diff --git a/tests/ui/doc/doc_comments_missing_terminal_punctuation_unfixable.stderr b/tests/ui/doc/doc_comments_missing_terminal_punctuation_unfixable.stderr new file mode 100644 index 000000000000..72e10b0bff9f --- /dev/null +++ b/tests/ui/doc/doc_comments_missing_terminal_punctuation_unfixable.stderr @@ -0,0 +1,20 @@ +error: doc comments should end with a terminal punctuation mark + --> tests/ui/doc/doc_comments_missing_terminal_punctuation_unfixable.rs:7:68 + | +LL | /// Sometimes the doc comment ends with parentheses (like this) + | ^ + | + = help: end the doc comment with some punctuation + = note: `-D clippy::doc-comments-missing-terminal-punctuation` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::doc_comments_missing_terminal_punctuation)]` + +error: doc comments should end with a terminal punctuation mark + --> tests/ui/doc/doc_comments_missing_terminal_punctuation_unfixable.rs:10:41 + | +LL | /// This comment ends with "a quote" + | ^ + | + = help: end the doc comment with some punctuation + +error: aborting due to 2 previous errors +