@@ -10,6 +10,8 @@ use pulldown_cmark::Event::{
1010use pulldown_cmark:: Tag :: { CodeBlock , Heading , Item , Link , Paragraph } ;
1111use pulldown_cmark:: { BrokenLink , CodeBlockKind , CowStr , Options } ;
1212use rustc_ast:: ast:: { Async , Attribute , Fn , FnRetTy , ItemKind } ;
13+ use rustc_ast:: token:: CommentKind ;
14+ use rustc_ast:: { AttrKind , AttrStyle } ;
1315use rustc_data_structures:: fx:: FxHashSet ;
1416use rustc_data_structures:: sync:: Lrc ;
1517use rustc_errors:: emitter:: EmitterWriter ;
@@ -260,6 +262,53 @@ declare_clippy_lint! {
260262 "`pub fn` or `pub trait` with `# Safety` docs"
261263}
262264
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+
263312#[ expect( clippy:: module_name_repetitions) ]
264313#[ derive( Clone ) ]
265314pub struct DocMarkdown {
@@ -284,6 +333,7 @@ impl_lint_pass!(DocMarkdown => [
284333 MISSING_PANICS_DOC ,
285334 NEEDLESS_DOCTEST_MAIN ,
286335 UNNECESSARY_SAFETY_DOC ,
336+ SUSPICIOUS_DOC_COMMENTS
287337] ) ;
288338
289339impl < ' tcx > LateLintPass < ' tcx > for DocMarkdown {
@@ -478,6 +528,8 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
478528 return None ;
479529 }
480530
531+ check_almost_inner_doc ( cx, attrs) ;
532+
481533 let ( fragments, _) = attrs_to_doc_fragments ( attrs. iter ( ) . map ( |attr| ( attr, None ) ) , true ) ;
482534 let mut doc = String :: new ( ) ;
483535 for fragment in & fragments {
@@ -506,6 +558,43 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
506558 ) )
507559}
508560
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+
509598const RUST_CODE : & [ & str ] = & [ "rust" , "no_run" , "should_panic" , "compile_fail" ] ;
510599
511600#[ allow( clippy:: too_many_lines) ] // Only a big match statement
0 commit comments