-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Add a doc_comments_missing_terminal_punctuation
lint
#15758
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
AudaciousAxiom
wants to merge
8
commits into
rust-lang:master
Choose a base branch
from
AudaciousAxiom:feat/doc_comments_missing_terminal_punctuation
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
1025af5
feat: introduce a `doc_comments_missing_terminal_punctuation` lint
AudaciousAxiom 88bb926
doc_comments_missing_terminal_punctuation: real md parser
notriddle 39cac5e
doc_comments_missing_terminal_punctuation: exclude doc attributes to …
AudaciousAxiom 6d083ea
doc_comments_missing_terminal_punctuation: exclude tables and code bl…
AudaciousAxiom ec5aa66
doc_comments_missing_terminal_punctuation: handle code spans properly
AudaciousAxiom 70a7dd6
doc_comments_missing_terminal_punctuation: remove the parens special …
AudaciousAxiom 5ca9ca1
doc_comments_missing_terminal_punctuation: accept punctuation in quotes
AudaciousAxiom 0df651d
doc_comments_missing_terminal_punctuation: treat some trailers as unf…
AudaciousAxiom File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
117 changes: 117 additions & 0 deletions
117
clippy_lints/src/doc/doc_comments_missing_terminal_punctuation.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
Comment on lines
+52
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Double parsing every doc comment is not ideal, could this be tweaked into a state machine struct that |
||
{ | ||
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, | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
166 changes: 166 additions & 0 deletions
166
tests/ui/doc/doc_comments_missing_terminal_punctuation.fixed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<T> { | ||
/// 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: | ||
/// <em>Raw HTML is allowed as well</em> | ||
RawHtml, | ||
/// The raw HTML exception actually does the right thing to autolinks: | ||
/// <https://spec.commonmark.org/0.31.2/#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; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding catching individual paragraphs, would it suffice to track the unconditional depth of start/end tags and check when it goes from 1 -> 0 due to a
TagEnd::Paragraph
?