Collect errors in cc_bindings_from_rs.

When errors happens, besides generating a comment, we also
insert the error in the error reporting.

Added a command line flag to dump the error summary to a file.

PiperOrigin-RevId: 653406651
Change-Id: Idf5f748aaeaae3dd8d9c5ff888688eb52fa4328e
diff --git a/cc_bindings_from_rs/BUILD b/cc_bindings_from_rs/BUILD
index 53fcbe2..5dc287a 100644
--- a/cc_bindings_from_rs/BUILD
+++ b/cc_bindings_from_rs/BUILD
@@ -58,6 +58,7 @@
         ":run_compiler",
         "//common:arc_anyhow",
         "//common:code_gen_utils",
+        "//common:error_report",
         "//common:token_stream_printer",
         "@crate_index//:clap",
         "@crate_index//:itertools",
diff --git a/cc_bindings_from_rs/bindings.rs b/cc_bindings_from_rs/bindings.rs
index f859065..2a9d7fa 100644
--- a/cc_bindings_from_rs/bindings.rs
+++ b/cc_bindings_from_rs/bindings.rs
@@ -18,7 +18,7 @@
     escape_non_identifier_chars, format_cc_ident, format_cc_includes, make_rs_ident, CcInclude,
     NamespaceQualifier,
 };
-use error_report::{anyhow, bail, ensure};
+use error_report::{anyhow, bail, ensure, ErrorReporting};
 use itertools::Itertools;
 use proc_macro2::{Ident, Literal, TokenStream};
 use quote::{format_ident, quote, ToTokens};
@@ -63,6 +63,10 @@
         #[input]
         fn crate_name_to_include_paths(&self) -> Rc<HashMap<Rc<str>, Vec<CcInclude>>>;
 
+        /// Error collector for generating reports of errors encountered during the generation of bindings.
+        #[input]
+        fn errors(&self) -> Rc<dyn ErrorReporting>;
+
         // TODO(b/262878759): Provide a set of enabled/disabled Crubit features.
         #[input]
         fn _features(&self) -> ();
@@ -2328,7 +2332,7 @@
                 AssocItemKind::Fn { .. } => db.format_fn(def_id).map(Some),
                 other => Err(anyhow!("Unsupported `impl` item kind: {other:?}")),
             };
-            result.unwrap_or_else(|err| Some(format_unsupported_def(tcx, def_id, err)))
+            result.unwrap_or_else(|err| Some(format_unsupported_def(db, def_id, err)))
         })
         .collect();
 
@@ -2531,7 +2535,13 @@
 
 /// Formats a C++ comment explaining why no bindings have been generated for
 /// `local_def_id`.
-fn format_unsupported_def(tcx: TyCtxt, local_def_id: LocalDefId, err: Error) -> ApiSnippets {
+fn format_unsupported_def(
+    db: &dyn BindingsGenerator<'_>,
+    local_def_id: LocalDefId,
+    err: Error,
+) -> ApiSnippets {
+    let tcx = db.tcx();
+    db.errors().insert(&err);
     let source_loc = format_source_location(tcx, local_def_id);
     let name = tcx.def_path_str(local_def_id.to_def_id());
 
@@ -2629,7 +2639,7 @@
         .filter_map(|item_id| {
             let def_id: LocalDefId = item_id.owner_id.def_id;
             db.format_item(def_id)
-                .unwrap_or_else(|err| Some(format_unsupported_def(tcx, def_id, err)))
+                .unwrap_or_else(|err| Some(format_unsupported_def(db, def_id, err)))
                 .map(|api_snippets| (def_id, api_snippets))
         })
         .sorted_by_key(|(def_id, _)| tcx.def_span(*def_id));
@@ -2744,6 +2754,7 @@
 
     use quote::quote;
 
+    use error_report::IgnoreErrors;
     use run_compiler_test_support::{find_def_id_by_name, run_compiler_for_testing};
     use token_stream_matchers::{
         assert_cc_matches, assert_cc_not_matches, assert_rs_matches, assert_rs_not_matches,
@@ -8415,6 +8426,7 @@
             tcx,
             /* crubit_support_path_format= */ "<crubit/support/for/tests/{header}>".into(),
             /* crate_name_to_include_paths= */ Default::default(),
+            /* errors = */ Rc::new(IgnoreErrors),
             /* _features= */ (),
         )
     }
diff --git a/cc_bindings_from_rs/cc_bindings_from_rs.rs b/cc_bindings_from_rs/cc_bindings_from_rs.rs
index 5ef8252..b95f326 100644
--- a/cc_bindings_from_rs/cc_bindings_from_rs.rs
+++ b/cc_bindings_from_rs/cc_bindings_from_rs.rs
@@ -18,6 +18,7 @@
 use bindings::Database;
 use cmdline::Cmdline;
 use code_gen_utils::CcInclude;
+use error_report::{ErrorReport, ErrorReporting, IgnoreErrors};
 use run_compiler::run_compiler;
 use token_stream_printer::{
     cc_tokens_to_formatted_string, rs_tokens_to_formatted_string, RustfmtConfig,
@@ -28,7 +29,11 @@
         .with_context(|| format!("Error when writing to {}", path.display()))
 }
 
-fn new_db<'tcx>(cmdline: &Cmdline, tcx: TyCtxt<'tcx>) -> Database<'tcx> {
+fn new_db<'tcx>(
+    cmdline: &Cmdline,
+    tcx: TyCtxt<'tcx>,
+    errors: Rc<dyn ErrorReporting>,
+) -> Database<'tcx> {
     let crubit_support_path_format = cmdline.crubit_support_path_format.as_str().into();
 
     let mut crate_name_to_include_paths = <HashMap<Rc<str>, Vec<CcInclude>>>::new();
@@ -41,14 +46,22 @@
         tcx,
         crubit_support_path_format,
         crate_name_to_include_paths.into(),
+        errors,
         /* _features= */ (),
     )
 }
 
 fn run_with_tcx(cmdline: &Cmdline, tcx: TyCtxt) -> Result<()> {
     use bindings::{generate_bindings, Output};
+
+    let errors: Rc<dyn ErrorReporting> = if cmdline.error_report_out.is_some() {
+        Rc::new(ErrorReport::new())
+    } else {
+        Rc::new(IgnoreErrors)
+    };
+
     let Output { h_body, rs_body } = {
-        let db = new_db(cmdline, tcx);
+        let db = new_db(cmdline, tcx, errors.clone());
         generate_bindings(&db)?
     };
 
@@ -64,6 +77,10 @@
         write_file(&cmdline.rs_out, &rs_body)?;
     }
 
+    if let Some(error_report_out) = &cmdline.error_report_out {
+        write_file(error_report_out, &errors.serialize_to_string().unwrap())?;
+    }
+
     Ok(())
 }
 
@@ -115,7 +132,9 @@
     /// Test data builder (see also
     /// https://testing.googleblog.com/2018/02/testing-on-toilet-cleanly-create-test.html).
     struct TestArgs {
+        rs_input: Option<String>,
         h_path: Option<String>,
+        error_report_out: Option<String>,
         extra_crubit_args: Vec<String>,
 
         /// Arg for the following `rustc` flag: `--codegen=panic=<arg>`.
@@ -133,12 +152,15 @@
     struct TestResult {
         h_path: PathBuf,
         rs_path: PathBuf,
+        error_report_out_path: Option<PathBuf>,
     }
 
     impl TestArgs {
         fn default_args() -> Result<Self> {
             Ok(Self {
+                rs_input: None,
                 h_path: None,
+                error_report_out: None,
                 extra_crubit_args: vec![],
                 panic_mechanism: "abort".to_string(),
                 extra_rustc_args: vec![],
@@ -153,6 +175,18 @@
             self
         }
 
+        /// Specify the path to the error report output file.
+        fn with_error_report_out(mut self, error_report_out: &str) -> Self {
+            self.error_report_out = Some(error_report_out.to_string());
+            self
+        }
+
+        /// Specify the test Rust input.
+        fn with_rs_input(mut self, rs_input: &str) -> Self {
+            self.rs_input = Some(rs_input.to_string());
+            self
+        }
+
         /// Replaces the default `--codegen=panic=abort` with the specified
         /// `panic_mechanism`.
         fn with_panic_mechanism(mut self, panic_mechanism: &str) -> Self {
@@ -185,11 +219,13 @@
                 None => self.tempdir.path().join("test_crate_cc_api.h"),
                 Some(s) => PathBuf::from(s),
             };
+
             let rs_path = self.tempdir.path().join("test_crate_cc_api_impl.rs");
 
             let rs_input_path = self.tempdir.path().join("test_crate.rs");
-            std::fs::write(
-                &rs_input_path,
+            let rs_input = if let Some(rs_input) = &self.rs_input {
+                rs_input
+            } else {
                 r#" pub mod public_module {
                         pub fn public_function() {
                             private_function()
@@ -197,8 +233,9 @@
 
                         fn private_function() {}
                     }
-                "#,
-            )?;
+                "#
+            };
+            std::fs::write(&rs_input_path, rs_input)?;
 
             let mut args = vec![
                 "cc_bindings_from_rs_unittest_executable".to_string(),
@@ -208,6 +245,15 @@
                 format!("--clang-format-exe-path={CLANG_FORMAT_EXE_PATH_FOR_TESTING}"),
                 format!("--rustfmt-exe-path={RUSTFMT_EXE_PATH_FOR_TESTING}"),
             ];
+
+            let mut error_report_out_path = None;
+            if let Some(error_report_out) = self.error_report_out.as_ref() {
+                error_report_out_path = Some(self.tempdir.path().join(error_report_out));
+                args.push(format!(
+                    "--error-report-out={}",
+                    error_report_out_path.as_ref().unwrap().display()
+                ));
+            }
             args.extend(self.extra_crubit_args.iter().cloned());
             args.extend([
                 "--".to_string(),
@@ -225,7 +271,7 @@
 
             run_with_cmdline_args(&args)?;
 
-            Ok(TestResult { h_path, rs_path })
+            Ok(TestResult { h_path, rs_path, error_report_out_path })
         }
     }
 
@@ -275,6 +321,31 @@
     }
 
     #[test]
+    fn test_error_reporting_generation() -> Result<()> {
+        let test_args =
+            TestArgs::default_args()?.with_error_report_out("error_report.json").with_rs_input(
+                r#"
+                pub use std::collections;
+                pub use std::path;
+                "#,
+            );
+
+        let test_result = test_args.run().expect("Error report generation should succeed");
+        assert!(test_result.error_report_out_path.is_some());
+        let error_report_out_path = test_result.error_report_out_path.as_ref().unwrap();
+        assert!(error_report_out_path.exists());
+        let error_report = std::fs::read_to_string(&error_report_out_path)?;
+        let expected_error_report = r#"{
+  "Unsupported rustc_hir::hir::ItemKind: {}": {
+    "count": 2,
+    "sample_message": "Unsupported rustc_hir::hir::ItemKind: `use` import"
+  }
+}"#;
+        assert_eq!(expected_error_report, error_report);
+        Ok(())
+    }
+
+    #[test]
     fn test_happy_path() -> Result<()> {
         let test_args = TestArgs::default_args()?;
         let test_result = test_args.run().expect("Default args should succeed");
diff --git a/cc_bindings_from_rs/cmdline.rs b/cc_bindings_from_rs/cmdline.rs
index f5f5067..d0ad460 100644
--- a/cc_bindings_from_rs/cmdline.rs
+++ b/cc_bindings_from_rs/cmdline.rs
@@ -60,6 +60,10 @@
     /// Command line arguments of the Rust compiler.
     #[clap(last = true, value_parser)]
     pub rustc_args: Vec<String>,
+
+    /// Path to the error reporting output file.
+    #[clap(long, value_parser, value_name = "FILE")]
+    pub error_report_out: Option<PathBuf>,
 }
 
 impl Cmdline {
@@ -229,6 +233,8 @@
           Path to a rustfmt executable that will be used to format the Rust source files generated by the tool
       --rustfmt-config-path <FILE>
           Path to a rustfmt.toml file that should replace the default formatting of the .rs files generated by the tool
+      --error-report-out <FILE>
+          Path to the error reporting output file
   -h, --help
           Print help
 "#;
diff --git a/common/error_report.rs b/common/error_report.rs
index 1081bea..198c99b 100644
--- a/common/error_report.rs
+++ b/common/error_report.rs
@@ -115,6 +115,7 @@
     /// shared freely.
     fn insert(&self, error: &arc_anyhow::Error);
     fn serialize_to_vec(&self) -> anyhow::Result<Vec<u8>>;
+    fn serialize_to_string(&self) -> anyhow::Result<String>;
 }
 
 /// A null [`ErrorReporting`] strategy.
@@ -127,6 +128,10 @@
     fn serialize_to_vec(&self) -> anyhow::Result<Vec<u8>> {
         Ok(vec![])
     }
+
+    fn serialize_to_string(&self) -> anyhow::Result<String> {
+        Ok(String::new())
+    }
 }
 
 /// An aggregate of zero or more errors.
@@ -165,6 +170,10 @@
     fn serialize_to_vec(&self) -> anyhow::Result<Vec<u8>> {
         Ok(serde_json::to_vec(&*self.map.borrow())?)
     }
+
+    fn serialize_to_string(&self) -> anyhow::Result<String> {
+        Ok(serde_json::to_string_pretty(&*self.map.borrow())?)
+    }
 }
 
 #[derive(Default, Debug, Serialize)]
@@ -371,7 +380,7 @@
         );
 
         assert_eq!(
-            serde_json::to_string_pretty(&*report.map.borrow()).unwrap(),
+            report.serialize_to_string().unwrap(),
             r#"{
   "abc{}": {
     "count": 2,