@@ -10,6 +10,8 @@ use pulldown_cmark::Event::{
10
10
use pulldown_cmark:: Tag :: { CodeBlock , Heading , Item , Link , Paragraph } ;
11
11
use pulldown_cmark:: { BrokenLink , CodeBlockKind , CowStr , Options } ;
12
12
use rustc_ast:: ast:: { Async , Attribute , Fn , FnRetTy , ItemKind } ;
13
+ use rustc_ast:: token:: CommentKind ;
14
+ use rustc_ast:: { AttrKind , AttrStyle } ;
13
15
use rustc_data_structures:: fx:: FxHashSet ;
14
16
use rustc_data_structures:: sync:: Lrc ;
15
17
use rustc_errors:: emitter:: EmitterWriter ;
@@ -260,6 +262,53 @@ declare_clippy_lint! {
260
262
"`pub fn` or `pub trait` with `# Safety` docs"
261
263
}
262
264
265
+ declare_clippy_lint ! {
266
+ /// ### What it does
267
+ /// Detects the use of outer doc comments (`///`, `/**`) followed by a bang (`!`): `///!`
268
+ ///
269
+ /// ### Why is this bad?
270
+ /// Triple-slash comments (known as "outer doc comments") apply to items that follow it.
271
+ /// An outer doc comment followed by a bang (i.e. `///!`) has no specific meaning.
272
+ ///
273
+ /// The user most likely meant to write an inner doc comment (`//!`, `/*!`), which
274
+ /// applies to the parent item (i.e. the item that the comment is contained in,
275
+ /// usually a module or crate).
276
+ ///
277
+ /// ### Known problems
278
+ /// Inner doc comments can only appear before items, so there are certain cases where the suggestion
279
+ /// made by this lint is not valid code. For example:
280
+ /// ```rs
281
+ /// fn foo() {}
282
+ /// ///!
283
+ /// fn bar() {}
284
+ /// ```
285
+ /// This lint detects the doc comment and suggests changing it to `//!`, but an inner doc comment
286
+ /// is not valid at that position.
287
+ ///
288
+ /// ### Example
289
+ /// In this example, the doc comment is attached to the *function*, rather than the *module*.
290
+ /// ```no_run
291
+ /// pub mod util {
292
+ /// ///! This module contains utility functions.
293
+ ///
294
+ /// pub fn dummy() {}
295
+ /// }
296
+ /// ```
297
+ ///
298
+ /// Use instead:
299
+ /// ```no_run
300
+ /// pub mod util {
301
+ /// //! This module contains utility functions.
302
+ ///
303
+ /// pub fn dummy() {}
304
+ /// }
305
+ /// ```
306
+ #[ clippy:: version = "1.70.0" ]
307
+ pub SUSPICIOUS_DOC_COMMENTS ,
308
+ suspicious,
309
+ "suspicious usage of (outer) doc comments"
310
+ }
311
+
263
312
#[ expect( clippy:: module_name_repetitions) ]
264
313
#[ derive( Clone ) ]
265
314
pub struct DocMarkdown {
@@ -284,6 +333,7 @@ impl_lint_pass!(DocMarkdown => [
284
333
MISSING_PANICS_DOC ,
285
334
NEEDLESS_DOCTEST_MAIN ,
286
335
UNNECESSARY_SAFETY_DOC ,
336
+ SUSPICIOUS_DOC_COMMENTS
287
337
] ) ;
288
338
289
339
impl < ' tcx > LateLintPass < ' tcx > for DocMarkdown {
@@ -478,6 +528,8 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
478
528
return None ;
479
529
}
480
530
531
+ check_almost_inner_doc ( cx, attrs) ;
532
+
481
533
let ( fragments, _) = attrs_to_doc_fragments ( attrs. iter ( ) . map ( |attr| ( attr, None ) ) , true ) ;
482
534
let mut doc = String :: new ( ) ;
483
535
for fragment in & fragments {
@@ -506,6 +558,43 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
506
558
) )
507
559
}
508
560
561
+ /// Looks for `///!` and `/**!` comments, which were probably meant to be `//!` and `/*!`
562
+ fn check_almost_inner_doc ( cx : & LateContext < ' _ > , attrs : & [ Attribute ] ) {
563
+ let replacements: Vec < _ > = attrs
564
+ . iter ( )
565
+ . filter_map ( |attr| {
566
+ if let AttrKind :: DocComment ( com_kind, sym) = attr. kind
567
+ && let AttrStyle :: Outer = attr. style
568
+ && let Some ( com) = sym. as_str ( ) . strip_prefix ( '!' )
569
+ {
570
+ let sugg = match com_kind {
571
+ CommentKind :: Line => format ! ( "//!{com}" ) ,
572
+ CommentKind :: Block => format ! ( "/*!{com}*/" ) ,
573
+ } ;
574
+ Some ( ( attr. span , sugg) )
575
+ } else {
576
+ None
577
+ }
578
+ } )
579
+ . collect ( ) ;
580
+
581
+ if let Some ( ( & ( lo_span, _) , & ( hi_span, _) ) ) = replacements. first ( ) . zip ( replacements. last ( ) ) {
582
+ span_lint_and_then (
583
+ cx,
584
+ SUSPICIOUS_DOC_COMMENTS ,
585
+ lo_span. to ( hi_span) ,
586
+ "this is an outer doc comment and does not apply to the parent module or crate" ,
587
+ |diag| {
588
+ diag. multipart_suggestion (
589
+ "use an inner doc comment to document the parent module or crate" ,
590
+ replacements,
591
+ Applicability :: MaybeIncorrect ,
592
+ ) ;
593
+ } ,
594
+ ) ;
595
+ }
596
+ }
597
+
509
598
const RUST_CODE : & [ & str ] = & [ "rust" , "no_run" , "should_panic" , "compile_fail" ] ;
510
599
511
600
#[ allow( clippy:: too_many_lines) ] // Only a big match statement
0 commit comments