about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--clippy_lints/src/declared_lints.rs1
-rw-r--r--clippy_lints/src/four_forward_slashes.rs99
-rw-r--r--clippy_lints/src/lib.rs2
-rw-r--r--tests/ui/four_forward_slashes.fixed48
-rw-r--r--tests/ui/four_forward_slashes.rs48
-rw-r--r--tests/ui/four_forward_slashes.stderr68
-rw-r--r--tests/ui/four_forward_slashes_first_line.fixed7
-rw-r--r--tests/ui/four_forward_slashes_first_line.rs7
-rw-r--r--tests/ui/four_forward_slashes_first_line.stderr15
10 files changed, 296 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ea8450ed5c2..db18bc296f7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4868,6 +4868,7 @@ Released 2018-09-13
 [`format_collect`]: https://rust-lang.github.io/rust-clippy/master/index.html#format_collect
 [`format_in_format_args`]: https://rust-lang.github.io/rust-clippy/master/index.html#format_in_format_args
 [`format_push_string`]: https://rust-lang.github.io/rust-clippy/master/index.html#format_push_string
+[`four_forward_slashes`]: https://rust-lang.github.io/rust-clippy/master/index.html#four_forward_slashes
 [`from_iter_instead_of_collect`]: https://rust-lang.github.io/rust-clippy/master/index.html#from_iter_instead_of_collect
 [`from_over_into`]: https://rust-lang.github.io/rust-clippy/master/index.html#from_over_into
 [`from_raw_with_void_ptr`]: https://rust-lang.github.io/rust-clippy/master/index.html#from_raw_with_void_ptr
diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs
index 7472158a81c..51035a9d66b 100644
--- a/clippy_lints/src/declared_lints.rs
+++ b/clippy_lints/src/declared_lints.rs
@@ -183,6 +183,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
     crate::formatting::SUSPICIOUS_ASSIGNMENT_FORMATTING_INFO,
     crate::formatting::SUSPICIOUS_ELSE_FORMATTING_INFO,
     crate::formatting::SUSPICIOUS_UNARY_OP_FORMATTING_INFO,
+    crate::four_forward_slashes::FOUR_FORWARD_SLASHES_INFO,
     crate::from_over_into::FROM_OVER_INTO_INFO,
     crate::from_raw_with_void_ptr::FROM_RAW_WITH_VOID_PTR_INFO,
     crate::from_str_radix_10::FROM_STR_RADIX_10_INFO,
diff --git a/clippy_lints/src/four_forward_slashes.rs b/clippy_lints/src/four_forward_slashes.rs
new file mode 100644
index 00000000000..419c7734344
--- /dev/null
+++ b/clippy_lints/src/four_forward_slashes.rs
@@ -0,0 +1,99 @@
+use clippy_utils::diagnostics::span_lint_and_then;
+use rustc_errors::Applicability;
+use rustc_hir::Item;
+use rustc_lint::{LateContext, LateLintPass, LintContext};
+use rustc_session::{declare_lint_pass, declare_tool_lint};
+use rustc_span::Span;
+
+declare_clippy_lint! {
+    /// ### What it does
+    /// Checks for outer doc comments written with 4 forward slashes (`////`).
+    ///
+    /// ### Why is this bad?
+    /// This is (probably) a typo, and results in it not being a doc comment; just a regular
+    /// comment.
+    ///
+    /// ### Example
+    /// ```rust
+    /// //// My amazing data structure
+    /// pub struct Foo {
+    ///     // ...
+    /// }
+    /// ```
+    ///
+    /// Use instead:
+    /// ```rust
+    /// /// My amazing data structure
+    /// pub struct Foo {
+    ///     // ...
+    /// }
+    /// ```
+    #[clippy::version = "1.72.0"]
+    pub FOUR_FORWARD_SLASHES,
+    suspicious,
+    "comments with 4 forward slashes (`////`) likely intended to be doc comments (`///`)"
+}
+declare_lint_pass!(FourForwardSlashes => [FOUR_FORWARD_SLASHES]);
+
+impl<'tcx> LateLintPass<'tcx> for FourForwardSlashes {
+    fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) {
+        if item.span.from_expansion() {
+            return;
+        }
+        let sm = cx.sess().source_map();
+        let mut span = cx
+            .tcx
+            .hir()
+            .attrs(item.hir_id())
+            .iter()
+            .fold(item.span.shrink_to_lo(), |span, attr| span.to(attr.span));
+        let (Some(file), _, _, end_line, _) = sm.span_to_location_info(span) else {
+            return;
+        };
+        let mut bad_comments = vec![];
+        for line in (0..end_line.saturating_sub(1)).rev() {
+            let Some(contents) = file.get_line(line).map(|c| c.trim().to_owned()) else {
+                return;
+            };
+            // Keep searching until we find the next item
+            if !contents.is_empty() && !contents.starts_with("//") && !contents.starts_with("#[") {
+                break;
+            }
+
+            if contents.starts_with("////") && !matches!(contents.chars().nth(4), Some('/' | '!')) {
+                let bounds = file.line_bounds(line);
+                let line_span = Span::with_root_ctxt(bounds.start, bounds.end);
+                span = line_span.to(span);
+                bad_comments.push((line_span, contents));
+            }
+        }
+
+        if !bad_comments.is_empty() {
+            span_lint_and_then(
+                cx,
+                FOUR_FORWARD_SLASHES,
+                span,
+                "this item has comments with 4 forward slashes (`////`). These look like doc comments, but they aren't",
+                |diag| {
+                    let msg = if bad_comments.len() == 1 {
+                        "make this a doc comment by removing one `/`"
+                    } else {
+                        "turn these into doc comments by removing one `/`"
+                    };
+
+                    diag.multipart_suggestion(
+                        msg,
+                        bad_comments
+                            .into_iter()
+                            // It's a little unfortunate but the span includes the `\n` yet the contents
+                            // do not, so we must add it back. If some codebase uses `\r\n` instead they
+                            // will need normalization but it should be fine
+                            .map(|(span, c)| (span, c.replacen("////", "///", 1) + "\n"))
+                            .collect(),
+                        Applicability::MachineApplicable,
+                    );
+                },
+            );
+        }
+    }
+}
diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs
index 5e62cfd70ec..4ea25e4be9d 100644
--- a/clippy_lints/src/lib.rs
+++ b/clippy_lints/src/lib.rs
@@ -136,6 +136,7 @@ mod format_args;
 mod format_impl;
 mod format_push_string;
 mod formatting;
+mod four_forward_slashes;
 mod from_over_into;
 mod from_raw_with_void_ptr;
 mod from_str_radix_10;
@@ -1078,6 +1079,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
     store.register_early_pass(|| Box::new(visibility::Visibility));
     store.register_late_pass(move |_| Box::new(tuple_array_conversions::TupleArrayConversions { msrv: msrv() }));
     store.register_late_pass(|_| Box::new(manual_float_methods::ManualFloatMethods));
+    store.register_late_pass(|_| Box::new(four_forward_slashes::FourForwardSlashes));
     // add lints here, do not remove this comment, it's used in `new_lint`
 }
 
diff --git a/tests/ui/four_forward_slashes.fixed b/tests/ui/four_forward_slashes.fixed
new file mode 100644
index 00000000000..54b2c414b62
--- /dev/null
+++ b/tests/ui/four_forward_slashes.fixed
@@ -0,0 +1,48 @@
+//@run-rustfix
+//@aux-build:proc_macros.rs:proc-macro
+#![feature(custom_inner_attributes)]
+#![allow(unused)]
+#![warn(clippy::four_forward_slashes)]
+#![no_main]
+#![rustfmt::skip]
+
+#[macro_use]
+extern crate proc_macros;
+
+/// whoops
+fn a() {}
+
+/// whoops
+#[allow(dead_code)]
+fn b() {}
+
+/// whoops
+/// two borked comments!
+#[track_caller]
+fn c() {}
+
+fn d() {}
+
+#[test]
+/// between attributes
+#[allow(dead_code)]
+fn g() {}
+
+/// not very start of contents
+fn h() {}
+
+fn i() {
+    //// don't lint me bozo
+    todo!()
+}
+
+external! {
+    //// don't lint me bozo
+    fn e() {}
+}
+
+with_span! {
+    span
+    //// don't lint me bozo
+    fn f() {}
+}
diff --git a/tests/ui/four_forward_slashes.rs b/tests/ui/four_forward_slashes.rs
new file mode 100644
index 00000000000..facdc8cb17d
--- /dev/null
+++ b/tests/ui/four_forward_slashes.rs
@@ -0,0 +1,48 @@
+//@run-rustfix
+//@aux-build:proc_macros.rs:proc-macro
+#![feature(custom_inner_attributes)]
+#![allow(unused)]
+#![warn(clippy::four_forward_slashes)]
+#![no_main]
+#![rustfmt::skip]
+
+#[macro_use]
+extern crate proc_macros;
+
+//// whoops
+fn a() {}
+
+//// whoops
+#[allow(dead_code)]
+fn b() {}
+
+//// whoops
+//// two borked comments!
+#[track_caller]
+fn c() {}
+
+fn d() {}
+
+#[test]
+//// between attributes
+#[allow(dead_code)]
+fn g() {}
+
+    //// not very start of contents
+fn h() {}
+
+fn i() {
+    //// don't lint me bozo
+    todo!()
+}
+
+external! {
+    //// don't lint me bozo
+    fn e() {}
+}
+
+with_span! {
+    span
+    //// don't lint me bozo
+    fn f() {}
+}
diff --git a/tests/ui/four_forward_slashes.stderr b/tests/ui/four_forward_slashes.stderr
new file mode 100644
index 00000000000..89162e6b010
--- /dev/null
+++ b/tests/ui/four_forward_slashes.stderr
@@ -0,0 +1,68 @@
+error: this item has comments with 4 forward slashes (`////`). These look like doc comments, but they aren't
+  --> $DIR/four_forward_slashes.rs:12:1
+   |
+LL | / //// whoops
+LL | | fn a() {}
+   | |_
+   |
+   = note: `-D clippy::four-forward-slashes` implied by `-D warnings`
+help: make this a doc comment by removing one `/`
+   |
+LL + /// whoops
+   |
+
+error: this item has comments with 4 forward slashes (`////`). These look like doc comments, but they aren't
+  --> $DIR/four_forward_slashes.rs:15:1
+   |
+LL | / //// whoops
+LL | | #[allow(dead_code)]
+LL | | fn b() {}
+   | |_
+   |
+help: make this a doc comment by removing one `/`
+   |
+LL + /// whoops
+   |
+
+error: this item has comments with 4 forward slashes (`////`). These look like doc comments, but they aren't
+  --> $DIR/four_forward_slashes.rs:19:1
+   |
+LL | / //// whoops
+LL | | //// two borked comments!
+LL | | #[track_caller]
+LL | | fn c() {}
+   | |_
+   |
+help: turn these into doc comments by removing one `/`
+   |
+LL + /// whoops
+LL ~ /// two borked comments!
+   |
+
+error: this item has comments with 4 forward slashes (`////`). These look like doc comments, but they aren't
+  --> $DIR/four_forward_slashes.rs:27:1
+   |
+LL | / //// between attributes
+LL | | #[allow(dead_code)]
+LL | | fn g() {}
+   | |_
+   |
+help: make this a doc comment by removing one `/`
+   |
+LL + /// between attributes
+   |
+
+error: this item has comments with 4 forward slashes (`////`). These look like doc comments, but they aren't
+  --> $DIR/four_forward_slashes.rs:31:1
+   |
+LL | /     //// not very start of contents
+LL | | fn h() {}
+   | |_
+   |
+help: make this a doc comment by removing one `/`
+   |
+LL + /// not very start of contents
+   |
+
+error: aborting due to 5 previous errors
+
diff --git a/tests/ui/four_forward_slashes_first_line.fixed b/tests/ui/four_forward_slashes_first_line.fixed
new file mode 100644
index 00000000000..ce272b4c6cd
--- /dev/null
+++ b/tests/ui/four_forward_slashes_first_line.fixed
@@ -0,0 +1,7 @@
+/// borked doc comment on the first line. doesn't combust!
+fn a() {}
+
+//@run-rustfix
+// This test's entire purpose is to make sure we don't panic if the comment with four slashes
+// extends to the first line of the file. This is likely pretty rare in production, but an ICE is an
+// ICE.
diff --git a/tests/ui/four_forward_slashes_first_line.rs b/tests/ui/four_forward_slashes_first_line.rs
new file mode 100644
index 00000000000..d8f82d4410b
--- /dev/null
+++ b/tests/ui/four_forward_slashes_first_line.rs
@@ -0,0 +1,7 @@
+//// borked doc comment on the first line. doesn't combust!
+fn a() {}
+
+//@run-rustfix
+// This test's entire purpose is to make sure we don't panic if the comment with four slashes
+// extends to the first line of the file. This is likely pretty rare in production, but an ICE is an
+// ICE.
diff --git a/tests/ui/four_forward_slashes_first_line.stderr b/tests/ui/four_forward_slashes_first_line.stderr
new file mode 100644
index 00000000000..7944da14feb
--- /dev/null
+++ b/tests/ui/four_forward_slashes_first_line.stderr
@@ -0,0 +1,15 @@
+error: this item has comments with 4 forward slashes (`////`). These look like doc comments, but they aren't
+  --> $DIR/four_forward_slashes_first_line.rs:1:1
+   |
+LL | / //// borked doc comment on the first line. doesn't combust!
+LL | | fn a() {}
+   | |_
+   |
+   = note: `-D clippy::four-forward-slashes` implied by `-D warnings`
+help: make this a doc comment by removing one `/`
+   |
+LL + /// borked doc comment on the first line. doesn't combust!
+   |
+
+error: aborting due to previous error
+