Repository: Enselic/cargo-public-api (rust)

Issue:
Inspecting the standard library
I would like a way to automatically find all the types in `std`, but naively pointing `cargo public-api` at `std` fails because it can't find `core` and other dependencies. 

```console
$ cargo public-api --manifest-path $(rustc --print sysroot)/lib/rustlib/src/rust/library/std/Cargo.toml
Error: Failed to build rustdoc JSON. Stderr:     Checking compiler_builtins v0.1.78
    Checking libc v0.2.127
   Compiling unwind v0.0.0 (/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/unwind)
error[E0433]: failed to resolve: could not find `prelude` in `core`

error[E0432]: unresolved import `core::ops`
 --> /home/michael/.cargo/registry/src/github.com-1ecc6299db9ec823/compiler_builtins-0.1.78/src/float/mod.rs:1:5
  |
1 | use core::ops;
  |     ^^^^^^^^^ no `ops` in the root

error[E0432]: unresolved import `core::intrinsics`
  --> /home/michael/.cargo/registry/src/github.com-1ecc6299db9ec823/compiler_builtins-0.1.78/src/mem/mod.rs:11:11
   |
11 | use core::intrinsics::{atomic_load_unordered, atomic_store_unordered, exact_div};
   |           ^^^^^^^^^^ could not find `intrinsics` in `core`

...
```

Presumably `rustc` does something funky with the sysroot that `cargo public-api` doesn't).

Do you have any suggestions for how I might be able to achieve this?

Pull request description:
Support `std` and `core` pre-built JSON via new `--rustdoc-json` option
First install the pre-built rustdoc JSON like this:

    rustup component add rust-docs-json --toolchain nightly

then you can use the new option to list the public API of e.g. `core`:

    cargo public-api --rustdoc-json ~/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/share/doc/rust/json/core.json

Closes #91 

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 140e610..bc34e0e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -495,9 +495,9 @@ dependencies = [
 
 [[package]]
 name = "rustdoc-types"
-version = "0.17.0"
+version = "0.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a61fb498f838b7da375722c088e3c2571c3fbe7fe38c8fa208a6de1a6fdc3907"
+checksum = "7745b68b43d89a088d2d270be09f54f948e587fc540cb15e3d3b75cb327a01c3"
 dependencies = [
  "serde",
 ]
diff --git a/cargo-public-api/src/git_utils.rs b/cargo-public-api/src/git_utils.rs
index 2251f23..651cb4c 100644
--- a/cargo-public-api/src/git_utils.rs
+++ b/cargo-public-api/src/git_utils.rs
@@ -7,7 +7,7 @@ use anyhow::{anyhow, Context, Result};
 
 /// Synchronously do a `git checkout` of `commit`.
 /// Returns the name of the original branch/commit.
-pub fn git_checkout(commit: &str, git_root: &Path, quiet: bool) -> Result<String> {
+pub fn git_checkout(commit: &str, git_root: &Path, quiet: bool, force: bool) -> Result<String> {
     let original_branch = current_branch_or_commit(&git_root)?;
 
     let mut command = Command::new("git");
@@ -16,6 +16,9 @@ pub fn git_checkout(commit: &str, git_root: &Path, quiet: bool) -> Result<String
     if quiet {
         command.arg("--quiet");
     }
+    if force {
+        command.arg("--force");
+    }
     if command.spawn()?.wait()?.success() {
         Ok(original_branch)
     } else {
diff --git a/cargo-public-api/src/main.rs b/cargo-public-api/src/main.rs
index 14bd51f..b7e5827 100644
--- a/cargo-public-api/src/main.rs
+++ b/cargo-public-api/src/main.rs
@@ -70,12 +70,30 @@ pub struct Args {
     #[clap(long, min_values = 2, max_values = 2)]
     diff_git_checkouts: Option<Vec<String>>,
 
+    /// Raise this flag to discard working tree changes during git checkouts when
+    /// `--diff-git-checkouts` is used.
+    #[clap(long)]
+    force_git_checkouts: bool,
+
     /// Usage: --diff-rustdoc-json <RUSTDOC_JSON_PATH_1> <RUSTDOC_JSON_PATH_2>
     ///
     /// Diff the public API across two different rustdoc JSON files.
     #[clap(long, min_values = 2, max_values = 2)]
     diff_rustdoc_json: Option<Vec<String>>,
 
+    /// Usage: --rustdoc-json <RUSTDOC_JSON_PATH>
+    ///
+    /// List the public API based on the given rustdoc JSON file. Try for example
+    ///
+    ///     rustup component add rust-docs-json --toolchain  nightly
+    ///
+    /// and then
+    ///
+    ///     cargo public-api --rustdoc-json ~/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/share/doc/rust/json/std.json
+    ///
+    #[clap(long)]
+    rustdoc_json: Option<String>,
+
     /// Exit with failure if the specified API diff is detected.
     ///
     /// * all = deny added, changed, and removed public items in the API
@@ -167,6 +185,8 @@ fn main_() -> Result<()> {
         print_diff_between_two_commits(&args, commits)?
     } else if let Some(files) = &args.diff_rustdoc_json {
         print_diff_between_two_rustdoc_json_files(&args, files)?
+    } else if let Some(rustdoc_json) = &args.rustdoc_json {
+        print_public_items_from_json(&args, rustdoc_json)?
     } else {
         print_public_items_of_current_commit(&args)?
     };
@@ -258,7 +278,19 @@ fn check_diff(args: &Args, diff: &Option<PublicItemsDiff>) -> Result<()> {
 
 fn print_public_items_of_current_commit(args: &Args) -> Result<PostProcessing> {
     let (public_api, branch_to_restore) = collect_public_api_from_commit(args, None)?;
+    print_public_items(args, &public_api, branch_to_restore)
+}
+
+fn print_public_items_from_json(args: &Args, json_path: &str) -> Result<PostProcessing> {
+    let public_api = public_api_from_rustdoc_json_path(json_path, args)?;
+    print_public_items(args, &public_api, None)
+}
 
+fn print_public_items(
+    args: &Args,
+    public_api: &PublicApi,
+    branch_to_restore: Option<String>,
+) -> Result<PostProcessing> {
     Plain::print_items(&mut stdout(), args, &public_api.items)?;
 
     Ok(PostProcessing {
@@ -310,7 +342,12 @@ fn print_diff(args: &Args, old: Vec<PublicItem>, new: Vec<PublicItem>) -> Result
 impl PostProcessing {
     fn perform(&self, args: &Args) -> Result<()> {
         if let Some(branch_to_restore) = &self.branch_to_restore {
-            git_utils::git_checkout(branch_to_restore, &args.git_root()?, !args.verbose)?;
+            git_utils::git_checkout(
+                branch_to_restore,
+                &args.git_root()?,
+                !args.verbose,
+                args.force_git_checkouts,
+            )?;
         }
 
         check_diff(args, &self.diff_to_check)
@@ -358,6 +395,7 @@ fn collect_public_api_from_commit(
             commit,
             &args.git_root()?,
             !args.verbose,
+            args.force_git_checkouts,
         )?)
     } else {
         None
diff --git a/public-api/Cargo.toml b/public-api/Cargo.toml
index f30d421..747ac23 100644
--- a/public-api/Cargo.toml
+++ b/public-api/Cargo.toml
@@ -23,7 +23,7 @@ features = ["unbounded_depth"]
 
 [dependencies.rustdoc-types]
 # path = "/Users/martin/src/rustdoc-types"
-version = "0.17.0"
+version = "0.18.0"
 
 [dev-dependencies]
 anyhow = "1.0.53"
diff --git a/public-api/src/render.rs b/public-api/src/render.rs
index 4da5f42..ab9fe5f 100644
--- a/public-api/src/render.rs
+++ b/public-api/src/render.rs
@@ -176,7 +176,21 @@ impl<'a> RenderingContext<'a> {
                 }
                 output
             }
-            ItemEnum::PrimitiveType(_) => self.render_simple(&["primitive", "type"], &item.path()),
+            ItemEnum::Primitive(primitive) => {
+                // This is hard to write tests for since only Rust `core` is
+                // allowed to define primitives. So you have to test this code
+                // using the pre-built rustdoc JSON for core:
+                //
+                //   rustup component add rust-docs-json --toolchain nightly
+                //   cargo run -- --rustdoc-json ~/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/share/doc/rust/json/core.json
+                let mut output = pub_();
+                output.extend([
+                    Token::kind("type"),
+                    ws!(),
+                    Token::primitive(&primitive.name),
+                ]);
+                output
+            }
         };
 
         tokens.extend(inner_tokens);


Test patch:
diff --git a/cargo-public-api/tests/cargo-public-api-bin-tests.rs b/cargo-public-api/tests/cargo-public-api-bin-tests.rs
index d45e4ed..5856353 100644
--- a/cargo-public-api/tests/cargo-public-api-bin-tests.rs
+++ b/cargo-public-api/tests/cargo-public-api-bin-tests.rs
@@ -26,6 +26,24 @@ use test_utils::rustdoc_json_path_for_crate;
 #[path = "../src/git_utils.rs"] // Say NO to copy-paste!
 mod git_utils;
 
+fn create_test_repo_with_dirty_git_tree() -> TestRepo {
+    let test_repo = TestRepo::new();
+
+    // Make the tree dirty by appending a comment to src/lib.rs
+    let mut lib_rs_path = test_repo.path.path().to_owned();
+    lib_rs_path.push("src/lib.rs");
+
+    let mut lib_rs = OpenOptions::new()
+        .write(true)
+        .append(true)
+        .open(&lib_rs_path)
+        .unwrap();
+
+    writeln!(lib_rs, "// Make git tree dirty").unwrap();
+
+    test_repo
+}
+
 // FIXME: This tests is ignored in CI due to some unknown issue with windows
 #[test]
 #[cfg_attr(all(target_family = "windows", in_ci), ignore)]
@@ -151,7 +169,7 @@ fn diff_public_items_detached_head() {
 
     // Detach HEAD
     let path = test_repo.path();
-    git_utils::git_checkout("v0.1.1", path, true).unwrap();
+    git_utils::git_checkout("v0.1.1", path, true, false).unwrap();
     assert_eq!(None, git_utils::current_branch(path).unwrap());
     let before = git_utils::current_commit(path).unwrap();
 
@@ -173,19 +191,7 @@ fn diff_public_items_detached_head() {
 #[test]
 #[cfg_attr(target_family = "windows", ignore)]
 fn diff_public_items_with_dirty_tree_fails() {
-    let test_repo = TestRepo::new();
-
-    // Make the tree dirty by appending a comment to src/lib.rs
-    let mut lib_rs_path = test_repo.path.path().to_owned();
-    lib_rs_path.push("src/lib.rs");
-
-    let mut lib_rs = OpenOptions::new()
-        .write(true)
-        .append(true)
-        .open(&lib_rs_path)
-        .unwrap();
-
-    writeln!(lib_rs, "// Make git tree dirty").unwrap();
+    let test_repo = create_test_repo_with_dirty_git_tree();
 
     // Make sure diffing does not destroy uncommitted data!
     let mut cmd = Command::cargo_bin("cargo-public-api").unwrap();
@@ -201,6 +207,25 @@ fn diff_public_items_with_dirty_tree_fails() {
         .failure();
 }
 
+/// Test that diffing succeedes if the git tree is dirty and
+/// `force-git-checkout` option is specified.
+#[test]
+#[cfg_attr(target_family = "windows", ignore)]
+fn diff_public_items_with_dirty_tree_succeedes_with_force_option() {
+    let test_repo = create_test_repo_with_dirty_git_tree();
+
+    let mut cmd = Command::cargo_bin("cargo-public-api").unwrap();
+    cmd.current_dir(&test_repo.path);
+    cmd.arg("--color=never");
+    cmd.arg("--diff-git-checkouts");
+    cmd.arg("v0.2.0");
+    cmd.arg("v0.3.0");
+    cmd.arg("--force-git-checkouts");
+    cmd.assert()
+        .stdout_or_bless("./tests/expected-output/example_api_diff_v0.2.0_to_v0.3.0.txt")
+        .success();
+}
+
 #[test]
 fn deny_when_not_diffing() {
     let mut cmd = TestCmd::new();
@@ -388,6 +413,17 @@ fn diff_public_items_from_files() {
         .success();
 }
 
+#[test]
+fn list_public_items_from_json_file() {
+    let json_file = rustdoc_json_path_for_crate("../test-apis/example_api-v0.3.0");
+    let mut cmd = Command::cargo_bin("cargo-public-api").unwrap();
+    cmd.arg("--rustdoc-json");
+    cmd.arg(json_file);
+    cmd.assert()
+        .stdout_or_bless("./tests/expected-output/example_api-v0.3.0.txt")
+        .success();
+}
+
 #[test]
 fn diff_public_items_missing_one_arg() {
     let mut cmd = TestCmd::new();