@@ -198,6 +198,29 @@ declare_clippy_lint! {
198198 "presence of `fn main() {` in code examples"
199199}
200200
201+ declare_clippy_lint ! {
202+ /// ### What it does
203+ /// Detects the syntax `['foo']` in documentation comments (notice quotes instead of backticks)
204+ /// outside of code blocks
205+ /// ### Why is this bad?
206+ /// It is likely a typo when defining an intra-doc link
207+ ///
208+ /// ### Example
209+ /// ```rust
210+ /// /// See also: ['foo']
211+ /// fn bar() {}
212+ /// ```
213+ /// Use instead:
214+ /// ```rust
215+ /// /// See also: [`foo`]
216+ /// fn bar() {}
217+ /// ```
218+ #[ clippy:: version = "1.63.0" ]
219+ pub DOC_LINK_WITH_QUOTES ,
220+ pedantic,
221+ "possible typo for an intra-doc link"
222+ }
223+
201224#[ expect( clippy:: module_name_repetitions) ]
202225#[ derive( Clone ) ]
203226pub struct DocMarkdown {
@@ -214,9 +237,14 @@ impl DocMarkdown {
214237 }
215238}
216239
217- impl_lint_pass ! ( DocMarkdown =>
218- [ DOC_MARKDOWN , MISSING_SAFETY_DOC , MISSING_ERRORS_DOC , MISSING_PANICS_DOC , NEEDLESS_DOCTEST_MAIN ]
219- ) ;
240+ impl_lint_pass ! ( DocMarkdown => [
241+ DOC_LINK_WITH_QUOTES ,
242+ DOC_MARKDOWN ,
243+ MISSING_SAFETY_DOC ,
244+ MISSING_ERRORS_DOC ,
245+ MISSING_PANICS_DOC ,
246+ NEEDLESS_DOCTEST_MAIN
247+ ] ) ;
220248
221249impl < ' tcx > LateLintPass < ' tcx > for DocMarkdown {
222250 fn check_crate ( & mut self , cx : & LateContext < ' tcx > ) {
@@ -432,7 +460,7 @@ pub fn strip_doc_comment_decoration(doc: &str, comment_kind: CommentKind, span:
432460 ( no_stars, sizes)
433461}
434462
435- #[ derive( Copy , Clone ) ]
463+ #[ derive( Copy , Clone , Default ) ]
436464struct DocHeaders {
437465 safety : bool ,
438466 errors : bool ,
@@ -476,11 +504,7 @@ fn check_attrs<'a>(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs
476504 }
477505
478506 if doc. is_empty ( ) {
479- return DocHeaders {
480- safety : false ,
481- errors : false ,
482- panics : false ,
483- } ;
507+ return DocHeaders :: default ( ) ;
484508 }
485509
486510 let mut cb = fake_broken_link_callback;
@@ -521,11 +545,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
521545 use pulldown_cmark:: Tag :: { CodeBlock , Heading , Item , Link , Paragraph } ;
522546 use pulldown_cmark:: { CodeBlockKind , CowStr } ;
523547
524- let mut headers = DocHeaders {
525- safety : false ,
526- errors : false ,
527- panics : false ,
528- } ;
548+ let mut headers = DocHeaders :: default ( ) ;
529549 let mut in_code = false ;
530550 let mut in_link = None ;
531551 let mut in_heading = false ;
@@ -612,6 +632,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
612632 check_code ( cx, & text, edition, span) ;
613633 }
614634 } else {
635+ check_link_quotes ( cx, in_link. is_some ( ) , trimmed_text, span, & range, begin, text. len ( ) ) ;
615636 // Adjust for the beginning of the current `Event`
616637 let span = span. with_lo ( span. lo ( ) + BytePos :: from_usize ( range. start - begin) ) ;
617638 text_to_check. push ( ( text, span) ) ;
@@ -622,6 +643,27 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
622643 headers
623644}
624645
646+ fn check_link_quotes (
647+ cx : & LateContext < ' _ > ,
648+ in_link : bool ,
649+ trimmed_text : & str ,
650+ span : Span ,
651+ range : & Range < usize > ,
652+ begin : usize ,
653+ text_len : usize ,
654+ ) {
655+ if in_link && trimmed_text. starts_with ( '\'' ) && trimmed_text. ends_with ( '\'' ) {
656+ // fix the span to only point at the text within the link
657+ let lo = span. lo ( ) + BytePos :: from_usize ( range. start - begin) ;
658+ span_lint (
659+ cx,
660+ DOC_LINK_WITH_QUOTES ,
661+ span. with_lo ( lo) . with_hi ( lo + BytePos :: from_usize ( text_len) ) ,
662+ "possible intra-doc link using quotes instead of backticks" ,
663+ ) ;
664+ }
665+ }
666+
625667fn get_current_span ( spans : & [ ( usize , Span ) ] , idx : usize ) -> ( usize , Span ) {
626668 let index = match spans. binary_search_by ( |c| c. 0 . cmp ( & idx) ) {
627669 Ok ( o) => o,
0 commit comments