Don't propagate tooling directives from doc comments into Rust bindings.

Doc comments should exclude tooling directives like `// NOLINTNEXTLINE`.

PiperOrigin-RevId: 424916890
diff --git a/rs_bindings_from_cc/importer.cc b/rs_bindings_from_cc/importer.cc
index 05f3016..313b148 100644
--- a/rs_bindings_from_cc/importer.cc
+++ b/rs_bindings_from_cc/importer.cc
@@ -24,6 +24,7 @@
 #include "third_party/absl/status/statusor.h"
 #include "third_party/absl/strings/cord.h"
 #include "third_party/absl/strings/str_cat.h"
+#include "third_party/absl/strings/str_join.h"
 #include "third_party/absl/strings/string_view.h"
 #include "third_party/absl/strings/substitute.h"
 #include "third_party/llvm/llvm-project/clang/include/clang/AST/ASTContext.h"
@@ -41,6 +42,7 @@
 #include "third_party/llvm/llvm-project/clang/include/clang/Sema/Sema.h"
 #include "third_party/llvm/llvm-project/llvm/include/llvm/ADT/Optional.h"
 #include "third_party/llvm/llvm-project/llvm/include/llvm/Support/Casting.h"
+#include "third_party/llvm/llvm-project/llvm/include/llvm/Support/Regex.h"
 #include "util/gtl/flat_map.h"
 
 namespace rs_bindings_from_cc {
@@ -596,6 +598,16 @@
   }
 }
 
+static bool ShouldKeepCommentLine(absl::string_view line) {
+  // Based on https://clang.llvm.org/extra/clang-tidy/:
+  llvm::Regex patterns_to_ignore(
+      "^[[:space:]/]*"  // Whitespace, or extra //
+      "(NOLINT|NOLINTNEXTLINE|NOLINTBEGIN|NOLINTEND)"
+      "(\\([^)[:space:]]*\\)?)?"  // Optional (...)
+      "[[:space:]]*$");           // Whitespace
+  return !patterns_to_ignore.match(line);
+}
+
 std::optional<std::string> Importer::GetComment(const clang::Decl* decl) const {
   // This does currently not distinguish between different types of comments.
   // In general it is not possible in C++ to reliably only extract doc comments.
@@ -606,9 +618,15 @@
 
   if (raw_comment == nullptr) {
     return {};
-  } else {
-    return raw_comment->getFormattedText(sm, sm.getDiagnostics());
   }
+
+  std::string raw_comment_text =
+      raw_comment->getFormattedText(sm, sm.getDiagnostics());
+  std::string cleaned_comment_text = absl::StrJoin(
+      absl::StrSplit(raw_comment_text, '\n', ShouldKeepCommentLine), "\n");
+  return cleaned_comment_text.empty()
+             ? std::nullopt
+             : std::optional<std::string>(std::move(cleaned_comment_text));
 }
 
 SourceLoc Importer::ConvertSourceLocation(clang::SourceLocation loc) const {
diff --git a/rs_bindings_from_cc/ir_from_cc_test.rs b/rs_bindings_from_cc/ir_from_cc_test.rs
index e116103..83c8004 100644
--- a/rs_bindings_from_cc/ir_from_cc_test.rs
+++ b/rs_bindings_from_cc/ir_from_cc_test.rs
@@ -362,6 +362,56 @@
 }
 
 #[test]
+fn test_doc_comment_vs_tooling_directives() -> Result<()> {
+    let ir = ir_from_cc(
+        r#" // Doc comment for `f1`
+            // NOLINTNEXTLINE(google3-readability-pass-trivial-by-value)
+            void f1();
+
+            // Doc comment for `f2`
+            // // NOLINT
+            void f2();
+
+            // // NOLINT
+            static void f3();
+
+            // Mid-sentence usage: [...] this is why we need NOLINT / virtual [...].
+            void f4();
+
+            // No closing paren still suppresses
+            // NOLINTNEXTLINE(google3-readability
+            void f5();
+
+            // Multiple, comma-separated directives listed in parens
+            // NOLINTNEXTLINE(foo,bar)
+            void f6();
+        "#,
+    )?;
+
+    let comments: HashMap<&str, Option<&str>> = ir
+        .functions()
+        .map(|f| {
+            if let UnqualifiedIdentifier::Identifier(id) = &f.name {
+                (id.identifier.as_str(), f.doc_comment.as_deref())
+            } else {
+                panic!("No constructors/destructors expected in this test.")
+            }
+        })
+        .collect();
+
+    assert_eq!(comments["f1"], Some("Doc comment for `f1`"));
+    assert_eq!(comments["f2"], Some("Doc comment for `f2`"));
+    assert_eq!(comments["f3"], None);
+    assert_eq!(
+        comments["f4"],
+        Some("Mid-sentence usage: [...] this is why we need NOLINT / virtual [...].")
+    );
+    assert_eq!(comments["f5"], Some("No closing paren still suppresses"));
+    assert_eq!(comments["f6"], Some("Multiple, comma-separated directives listed in parens"));
+    Ok(())
+}
+
+#[test]
 fn test_type_conversion() -> Result<()> {
     // TODO(mboehme): Add tests for the corresponding versions of the types in
     // the `std` namespace. We currently can't do this because we can't include
diff --git a/rs_bindings_from_cc/test/golden/doc_comment_rs_api.rs b/rs_bindings_from_cc/test/golden/doc_comment_rs_api.rs
index 7ec574c..ad627d5 100644
--- a/rs_bindings_from_cc/test/golden/doc_comment_rs_api.rs
+++ b/rs_bindings_from_cc/test/golden/doc_comment_rs_api.rs
@@ -44,7 +44,6 @@
 
 /// An implicit conversion constructor which will get translated into `impl
 /// From<int> for DocCommentSlashes`.
-/// NOLINTNEXTLINE(google-explicit-constructor)
 impl From<i32> for DocCommentSlashes {
     #[inline(always)]
     fn from(__param_0: i32) -> Self {