1+ use clippy_utils:: diagnostics:: span_lint_and_then;
12use rustc_errors:: Applicability ;
23use rustc_lint:: LateContext ;
3- use rustc_resolve:: rustdoc:: main_body_opts ;
4-
5- use rustc_resolve :: rustdoc :: pulldown_cmark :: { Event , Options , Parser , Tag , TagEnd } ;
4+ use rustc_resolve:: rustdoc:: pulldown_cmark :: { Event , Tag , TagEnd } ;
5+ use rustc_span :: Span ;
6+ use std :: ops :: Range ;
67
78use super :: { DOC_PARAGRAPHS_MISSING_PUNCTUATION , Fragments } ;
89
9- const MSG : & str = "doc paragraphs should end with a terminal punctuation mark" ;
10- const PUNCTUATION_SUGGESTION : char = '.' ;
11-
12- pub fn check ( cx : & LateContext < ' _ > , doc : & str , fragments : Fragments < ' _ > ) {
13- for missing_punctuation in is_missing_punctuation ( doc) {
14- match missing_punctuation {
15- MissingPunctuation :: Fixable ( offset) => {
16- // This ignores `#[doc]` attributes, which we do not handle.
17- if let Some ( span) = fragments. span ( cx, offset..offset) {
18- clippy_utils:: diagnostics:: span_lint_and_sugg (
19- cx,
20- DOC_PARAGRAPHS_MISSING_PUNCTUATION ,
21- span,
22- MSG ,
23- "end the paragraph with some punctuation" ,
24- PUNCTUATION_SUGGESTION . to_string ( ) ,
25- Applicability :: MaybeIncorrect ,
26- ) ;
27- }
28- } ,
29- MissingPunctuation :: Unfixable ( offset) => {
30- // This ignores `#[doc]` attributes, which we do not handle.
31- if let Some ( span) = fragments. span ( cx, offset..offset) {
32- clippy_utils:: diagnostics:: span_lint_and_help (
33- cx,
34- DOC_PARAGRAPHS_MISSING_PUNCTUATION ,
35- span,
36- MSG ,
37- None ,
38- "end the paragraph with some punctuation" ,
39- ) ;
40- }
41- } ,
42- }
43- }
10+ #[ derive( Default ) ]
11+ pub ( super ) struct MissingPunctuation {
12+ no_report_depth : u32 ,
13+ current_paragraph : Option < Position > ,
4414}
4515
46- #[ must_use]
47- /// If punctuation is missing, returns the offset where new punctuation should be inserted.
48- fn is_missing_punctuation ( doc_string : & str ) -> Vec < MissingPunctuation > {
49- // The colon is not exactly a terminal punctuation mark, but this is required for paragraphs that
50- // introduce a table or a list for example.
51- const TERMINAL_PUNCTUATION_MARKS : & [ char ] = & [ '.' , '?' , '!' , '…' , ':' ] ;
16+ impl MissingPunctuation {
17+ pub fn check (
18+ & mut self ,
19+ cx : & LateContext < ' _ > ,
20+ event : & Event < ' _ > ,
21+ range : Range < usize > ,
22+ doc : & str ,
23+ fragments : Fragments < ' _ > ,
24+ ) {
25+ // The colon is not exactly a terminal punctuation mark, but this is required for paragraphs that
26+ // introduce a table or a list for example.
27+ const TERMINAL_PUNCTUATION_MARKS : & [ char ] = & [ '.' , '?' , '!' , '…' , ':' ] ;
5228
53- let mut no_report_depth = 0 ;
54- let mut missing_punctuation = Vec :: new ( ) ;
55- let mut current_paragraph = None ;
56-
57- for ( event, offset) in
58- Parser :: new_ext ( doc_string, main_body_opts ( ) - Options :: ENABLE_SMART_PUNCTUATION ) . into_offset_iter ( )
59- {
6029 match event {
6130 Event :: Start (
6231 Tag :: CodeBlock ( ..)
@@ -66,61 +35,82 @@ fn is_missing_punctuation(doc_string: &str) -> Vec<MissingPunctuation> {
6635 | Tag :: List ( ..)
6736 | Tag :: Table ( _) ,
6837 ) => {
69- no_report_depth += 1 ;
38+ self . no_report_depth += 1 ;
7039 } ,
7140 Event :: End ( TagEnd :: FootnoteDefinition ) => {
72- no_report_depth -= 1 ;
41+ self . no_report_depth -= 1 ;
7342 } ,
7443 Event :: End (
7544 TagEnd :: CodeBlock | TagEnd :: Heading ( _) | TagEnd :: HtmlBlock | TagEnd :: List ( _) | TagEnd :: Table ,
7645 ) => {
77- no_report_depth -= 1 ;
78- current_paragraph = None ;
46+ self . no_report_depth -= 1 ;
47+ self . current_paragraph = None ;
7948 } ,
8049 Event :: InlineHtml ( _) | Event :: Start ( Tag :: Image { .. } ) | Event :: End ( TagEnd :: Image ) => {
81- current_paragraph = None ;
50+ self . current_paragraph = None ;
8251 } ,
8352 Event :: End ( TagEnd :: Paragraph ) => {
84- if let Some ( mp) = current_paragraph {
85- missing_punctuation. push ( mp) ;
53+ if let Some ( position) = self . current_paragraph
54+ && let Some ( span) = position. span ( cx, fragments)
55+ {
56+ span_lint_and_then (
57+ cx,
58+ DOC_PARAGRAPHS_MISSING_PUNCTUATION ,
59+ span,
60+ "doc paragraphs should end with a terminal punctuation mark" ,
61+ |diag| {
62+ if matches ! ( position, Position :: Fixable ( _) ) {
63+ diag. span_suggestion (
64+ span,
65+ "end the paragraph with some punctuation" ,
66+ '.' ,
67+ Applicability :: MaybeIncorrect ,
68+ ) ;
69+ } else {
70+ diag. help ( "end the paragraph with some punctuation" ) ;
71+ }
72+ } ,
73+ ) ;
8674 }
8775 } ,
8876 Event :: Code ( ..) | Event :: Start ( Tag :: Link { .. } ) | Event :: End ( TagEnd :: Link )
89- if no_report_depth == 0 && !offset . is_empty ( ) =>
77+ if self . no_report_depth == 0 && !range . is_empty ( ) =>
9078 {
91- if doc_string[ ..offset. end ]
92- . trim_end ( )
93- . ends_with ( TERMINAL_PUNCTUATION_MARKS )
94- {
95- current_paragraph = None ;
79+ if doc[ ..range. end ] . trim_end ( ) . ends_with ( TERMINAL_PUNCTUATION_MARKS ) {
80+ self . current_paragraph = None ;
9681 } else {
97- current_paragraph = Some ( MissingPunctuation :: Fixable ( offset . end ) ) ;
82+ self . current_paragraph = Some ( Position :: Fixable ( range . end ) ) ;
9883 }
9984 } ,
100- Event :: Text ( ..) if no_report_depth == 0 && !offset . is_empty ( ) => {
101- let trimmed = doc_string [ ..offset . end ] . trim_end ( ) ;
85+ Event :: Text ( ..) if self . no_report_depth == 0 && !range . is_empty ( ) => {
86+ let trimmed = doc [ ..range . end ] . trim_end ( ) ;
10287 if trimmed. ends_with ( TERMINAL_PUNCTUATION_MARKS ) {
103- current_paragraph = None ;
88+ self . current_paragraph = None ;
10489 } else if let Some ( t) = trimmed. strip_suffix ( |c| c == ')' || c == '"' ) {
10590 if t. ends_with ( TERMINAL_PUNCTUATION_MARKS ) {
10691 // Avoid false positives.
107- current_paragraph = None ;
92+ self . current_paragraph = None ;
10893 } else {
109- current_paragraph = Some ( MissingPunctuation :: Unfixable ( offset . end ) ) ;
94+ self . current_paragraph = Some ( Position :: Unfixable ( range . end ) ) ;
11095 }
11196 } else {
112- current_paragraph = Some ( MissingPunctuation :: Fixable ( offset . end ) ) ;
97+ self . current_paragraph = Some ( Position :: Fixable ( range . end ) ) ;
11398 }
11499 } ,
115100 _ => { } ,
116101 }
117102 }
118-
119- missing_punctuation
120103}
121104
122105#[ derive( Debug , Copy , Clone , PartialEq , Eq ) ]
123- enum MissingPunctuation {
106+ enum Position {
124107 Fixable ( usize ) ,
125108 Unfixable ( usize ) ,
126109}
110+
111+ impl Position {
112+ fn span ( self , cx : & LateContext < ' _ > , fragments : Fragments < ' _ > ) -> Option < Span > {
113+ let ( Position :: Fixable ( pos) | Position :: Unfixable ( pos) ) = self ;
114+ fragments. span ( cx, pos..pos)
115+ }
116+ }
0 commit comments