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

Issue:
Group items even if multiple inherent `impl`s are used
```rust
pub struct pdbtbx::PDB
pub struct field pdbtbx::PDB::identifier: core::option::Option<alloc::string::String>
... (more fields)
impl pdbtbx::PDB
impl pdbtbx::PDB
impl pdbtbx::PDB
impl<'a> pdbtbx::PDB
```

This example is from the crate `pdbtbx` where I have used multiple impl blocks on a single struct to allow fro grouping of functions with some general comments about those groups. That is why it stands out so much. But I would assume these impl blocks (just the main block with all attached functions) to not be shown in the list here. An argument could be made to have the functions grouped below their respective impl blocks, especially when generic bounds are used. But this seems to not be the case here.

Pull request description:
Properly group multiple inherent `impl`s
"properly" meaning "group items like rustdoc HTML does it", which means each inherent `impl` is presented separately (i.e. no de-duplication)

Closes #251

Depends on #269


Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index 62637fa..0000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-# Please see the documentation for all configuration options:
-# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
-
-version: 2
-updates:
-  - package-ecosystem: "cargo"
-    directory: "/"
-    schedule:
-      interval: "weekly"
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index 3f82cec..ed1563e 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -15,14 +15,14 @@ jobs:
     name: cargo fmt -- --check
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v2
       - run: cargo fmt -- --check
 
   cargo-doc:
     name: RUSTDOCFLAGS='--deny warnings' cargo doc
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v2
       - uses: Swatinem/rust-cache@v2
       - run: RUSTDOCFLAGS='--deny warnings' cargo doc --locked --no-deps --document-private-items
 
@@ -30,7 +30,7 @@ jobs:
     name: cargo clippy -- --deny clippy::all --deny clippy::pedantic ...
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v2
       - uses: Swatinem/rust-cache@v2
       - run: scripts/cargo-clippy.sh
 
@@ -38,7 +38,7 @@ jobs:
     name: cargo audit
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v2
       - run: scripts/cargo-audit.sh
 
   cargo-test:
@@ -52,11 +52,11 @@ jobs:
     name: cargo test (${{ matrix.os }})
     runs-on: ${{ matrix.os }}
     steps:
-      - uses: actions/checkout@v3
-      - uses: dtolnay/rust-toolchain@e645b0cf01249a964ec099494d38d2da0f0b349f
+      - uses: actions/checkout@v2
+      - uses: actions-rs/toolchain@v1
         with:
           toolchain: nightly
-      - run: rustup default stable
+          profile: minimal
       - uses: Swatinem/rust-cache@v2
       - run: cargo test --locked
       - run: cargo build --locked --no-default-features # Build without "diff-latest" feature
diff --git a/.github/workflows/Nightly.yml b/.github/workflows/Nightly.yml
index d7d2413..24511df 100644
--- a/.github/workflows/Nightly.yml
+++ b/.github/workflows/Nightly.yml
@@ -32,11 +32,11 @@ jobs:
     environment: CICD-fork
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
-      - uses: dtolnay/rust-toolchain@e645b0cf01249a964ec099494d38d2da0f0b349f
+      - uses: actions/checkout@v2
+      - uses: actions-rs/toolchain@v1
         with:
           toolchain: nightly
-      - run: rustup default stable
+          profile: minimal
       - uses: Swatinem/rust-cache@v2
       - run: ./scripts/bless-expected-output-for-tests.sh
       - id: latest-nightly
diff --git a/.github/workflows/Release-cargo-public-api.yml b/.github/workflows/Release-cargo-public-api.yml
index a058262..75939f7 100644
--- a/.github/workflows/Release-cargo-public-api.yml
+++ b/.github/workflows/Release-cargo-public-api.yml
@@ -15,7 +15,7 @@ jobs:
     environment: crates.io
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v2
 
       # Figure out what tag to use. Do this first since we want to ensure that
       # the same tag is used for both public-api and cargo-public-api before we
@@ -31,8 +31,8 @@ jobs:
             echo "Version mismatch! Keep public-api and cargo-public-api versions in sync before you release! $version != $version2"
             exit 42
           fi
-          echo "VERSION=${version}" >> $GITHUB_OUTPUT
-          echo "GIT_TAG=v${version}" >> $GITHUB_OUTPUT
+          echo ::set-output name=VERSION::${version}
+          echo ::set-output name=GIT_TAG::v${version}
 
       # Try to cargo publish public-api. If this succeeds we will tag the
       # release. This is because we don't want to have a situation where a
@@ -61,7 +61,7 @@ jobs:
 
       # OK, both public-api and cargo-public-api has been published under the
       # same version. Create a GitHub Release now.
-      - uses: softprops/action-gh-release@d4e8205d7e959a9107da6396278b2f1f07af0f9b # 2022-12-09
+      - uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v0.1.14
         with:
           tag_name: ${{ steps.version.outputs.GIT_TAG }}
           generate_release_notes: true
diff --git a/.github/workflows/Release-rustdoc-json.yml b/.github/workflows/Release-rustdoc-json.yml
index 080e727..8bcb36e 100644
--- a/.github/workflows/Release-rustdoc-json.yml
+++ b/.github/workflows/Release-rustdoc-json.yml
@@ -15,15 +15,15 @@ jobs:
     environment: crates.io
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v2
 
       # Figure out what tag to use
       - name: calculate version
         id: version
         run: |
           version=$(cargo read-manifest --manifest-path rustdoc-json/Cargo.toml | jq --raw-output .version)
-          echo "VERSION=${version}" >> $GITHUB_OUTPUT
-          echo "GIT_TAG=rustdoc-json-v${version}" >> $GITHUB_OUTPUT
+          echo ::set-output name=VERSION::${version}
+          echo ::set-output name=GIT_TAG::rustdoc-json-v${version}
 
       # Try to cargo publish rustdoc-json. If this succeeds we will tag the
       # release. This is because we don't want to have a situation where a
diff --git a/.github/workflows/Release-rustup-toolchain.yml b/.github/workflows/Release-rustup-toolchain.yml
index 5f06210..3fb6564 100644
--- a/.github/workflows/Release-rustup-toolchain.yml
+++ b/.github/workflows/Release-rustup-toolchain.yml
@@ -15,15 +15,15 @@ jobs:
     environment: crates.io
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v2
 
       # Figure out what tag to use
       - name: calculate version
         id: version
         run: |
           version=$(cargo read-manifest --manifest-path rustup-toolchain/Cargo.toml | jq --raw-output .version)
-          echo "VERSION=${version}" >> $GITHUB_OUTPUT
-          echo "GIT_TAG=rustup-toolchain-v${version}" >> $GITHUB_OUTPUT
+          echo ::set-output name=VERSION::${version}
+          echo ::set-output name=GIT_TAG::rustup-toolchain-v${version}
 
       # Try to cargo publish rustup-toolchain. If this succeeds we will tag the
       # release. This is because we don't want to have a situation where a
diff --git a/Cargo.lock b/Cargo.lock
index ed0123b..43c49a4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -553,9 +553,9 @@ checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
 
 [[package]]
 name = "predicates"
-version = "2.1.5"
+version = "2.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd"
+checksum = "f54fc5dc63ed3bbf19494623db4f3af16842c0d975818e469022d09e53f0aa05"
 dependencies = [
  "difflib",
  "itertools",
diff --git a/cargo-public-api/Cargo.toml b/cargo-public-api/Cargo.toml
index bf1a814..99d16b4 100644
--- a/cargo-public-api/Cargo.toml
+++ b/cargo-public-api/Cargo.toml
@@ -50,7 +50,7 @@ path = "../rustup-toolchain"
 version = "0.1.1"
 
 [dev-dependencies.predicates]
-version = "2.1.5"
+version = "2.1.4"
 default-features = false
 
 [dev-dependencies]
diff --git a/docs/CI-EXAMPLES.md b/docs/CI-EXAMPLES.md
index 679f113..fd8f23f 100644
--- a/docs/CI-EXAMPLES.md
+++ b/docs/CI-EXAMPLES.md
@@ -14,15 +14,15 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       # Full git history needed
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v2
         with:
           fetch-depth: 0
 
       # Install nightly (stable is already installed)
-      # Note that dtolnay/rust-toolchain also sets this toolchain as the default, change with `rustup default <toolchain>`
-      - uses: dtolnay/rust-toolchain@master
+      - uses: actions-rs/toolchain@v1
         with:
           toolchain: nightly
+          profile: minimal
 
       # Install and run cargo public-api and deny any API diff
       - run: cargo install cargo-public-api
@@ -49,10 +49,10 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       # Install nightly (stable is already installed)
-      # Note that dtolnay/rust-toolchain also sets this toolchain as the default, change with `rustup default <toolchain>`
-      - uses: dtolnay/rust-toolchain@master
+      - uses: actions-rs/toolchain@v1
         with:
           toolchain: nightly
+          profile: minimal
 
       # Install and run cargo public-api and deny any API diff
       - run: cargo install cargo-public-api@0.22.0
diff --git a/public-api/CHANGELOG.md b/public-api/CHANGELOG.md
index 87d99d3..05739fa 100644
--- a/public-api/CHANGELOG.md
+++ b/public-api/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 If a version is not listed below, it means it had no API changes.
 
+## Unreleased v0.27.0
+* Make `Eq` and `Hash` for `PublicItem` only take tokens into account, to make diffing insensitive to internal data that has no bearing on public API surface of a crate
+* Remove `Ord` and `PartialOrd` impls of `PublicItem` and `ChangedPublicItem`. Use `PublicItem::grouping_cmp` and `ChangedPublicItem::grouping_cmp` instead.
+
 ## v0.26.0
 * Put auto-derived impls (`Clone`, `Debug`, etc) in a separate group, right after normal `impl`s
 * Remove deprecated `Options::with_blanket_implementations`
diff --git a/public-api/public-api.txt b/public-api/public-api.txt
index f2b8072..ee1c89c 100644
--- a/public-api/public-api.txt
+++ b/public-api/public-api.txt
@@ -3,15 +3,13 @@ pub mod public_api::diff
 pub struct public_api::diff::ChangedPublicItem
 pub public_api::diff::ChangedPublicItem::new: public_api::PublicItem
 pub public_api::diff::ChangedPublicItem::old: public_api::PublicItem
+impl public_api::diff::ChangedPublicItem
+pub fn public_api::diff::ChangedPublicItem::grouping_cmp(&self, other: &Self) -> core::cmp::Ordering
 impl core::clone::Clone for public_api::diff::ChangedPublicItem
 pub fn public_api::diff::ChangedPublicItem::clone(&self) -> public_api::diff::ChangedPublicItem
 impl core::cmp::Eq for public_api::diff::ChangedPublicItem
-impl core::cmp::Ord for public_api::diff::ChangedPublicItem
-pub fn public_api::diff::ChangedPublicItem::cmp(&self, other: &public_api::diff::ChangedPublicItem) -> core::cmp::Ordering
 impl core::cmp::PartialEq<public_api::diff::ChangedPublicItem> for public_api::diff::ChangedPublicItem
 pub fn public_api::diff::ChangedPublicItem::eq(&self, other: &public_api::diff::ChangedPublicItem) -> bool
-impl core::cmp::PartialOrd<public_api::diff::ChangedPublicItem> for public_api::diff::ChangedPublicItem
-pub fn public_api::diff::ChangedPublicItem::partial_cmp(&self, other: &public_api::diff::ChangedPublicItem) -> core::option::Option<core::cmp::Ordering>
 impl core::fmt::Debug for public_api::diff::ChangedPublicItem
 pub fn public_api::diff::ChangedPublicItem::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
 impl core::marker::StructuralEq for public_api::diff::ChangedPublicItem
@@ -252,24 +250,19 @@ impl<T> core::convert::From<T> for public_api::PublicApi
 pub fn public_api::PublicApi::from(t: T) -> T
 pub struct public_api::PublicItem
 impl public_api::PublicItem
+pub fn public_api::PublicItem::grouping_cmp(&self, other: &Self) -> core::cmp::Ordering
 pub fn public_api::PublicItem::tokens(&self) -> impl core::iter::traits::iterator::Iterator<Item = &public_api::tokens::Token>
-impl core::cmp::Ord for public_api::PublicItem
-pub fn public_api::PublicItem::cmp(&self, other: &Self) -> core::cmp::Ordering
-impl core::cmp::PartialOrd<public_api::PublicItem> for public_api::PublicItem
-pub fn public_api::PublicItem::partial_cmp(&self, other: &Self) -> core::option::Option<core::cmp::Ordering>
+impl core::cmp::Eq for public_api::PublicItem
+impl core::cmp::PartialEq<public_api::PublicItem> for public_api::PublicItem
+pub fn public_api::PublicItem::eq(&self, other: &Self) -> bool
 impl core::fmt::Debug for public_api::PublicItem
 pub fn public_api::PublicItem::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
 impl core::fmt::Display for public_api::PublicItem
 pub fn public_api::PublicItem::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::hash::Hash for public_api::PublicItem
+pub fn public_api::PublicItem::hash<H: core::hash::Hasher>(&self, state: &mut H)
 impl core::clone::Clone for public_api::PublicItem
 pub fn public_api::PublicItem::clone(&self) -> public_api::PublicItem
-impl core::cmp::Eq for public_api::PublicItem
-impl core::cmp::PartialEq<public_api::PublicItem> for public_api::PublicItem
-pub fn public_api::PublicItem::eq(&self, other: &public_api::PublicItem) -> bool
-impl core::hash::Hash for public_api::PublicItem
-pub fn public_api::PublicItem::hash<__H: core::hash::Hasher>(&self, state: &mut __H) -> ()
-impl core::marker::StructuralEq for public_api::PublicItem
-impl core::marker::StructuralPartialEq for public_api::PublicItem
 impl core::marker::Send for public_api::PublicItem
 impl core::marker::Sync for public_api::PublicItem
 impl core::marker::Unpin for public_api::PublicItem
diff --git a/public-api/src/diff.rs b/public-api/src/diff.rs
index 4763b0b..9b79824 100644
--- a/public-api/src/diff.rs
+++ b/public-api/src/diff.rs
@@ -14,7 +14,7 @@ type ItemsWithPath = HashMap<PublicItemPath, Vec<PublicItem>>;
 
 /// An item has changed in the public API. Two [`PublicItem`]s are considered
 /// the same if their `path` is the same.
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub struct ChangedPublicItem {
     /// How the item used to look.
     pub old: PublicItem,
@@ -23,6 +23,17 @@ pub struct ChangedPublicItem {
     pub new: PublicItem,
 }
 
+impl ChangedPublicItem {
+    /// See [`PublicItem::grouping_cmp`]
+    #[must_use]
+    pub fn grouping_cmp(&self, other: &Self) -> std::cmp::Ordering {
+        match PublicItem::grouping_cmp(&self.old, &other.old) {
+            std::cmp::Ordering::Equal => PublicItem::grouping_cmp(&self.new, &other.new),
+            ordering => ordering,
+        }
+    }
+}
+
 /// The return value of [`Self::between`]. To quickly get a sense of what it
 /// contains, you can pretty-print it:
 /// ```txt
@@ -99,9 +110,9 @@ impl PublicApiDiff {
         }
 
         // Make output predictable and stable
-        removed.sort();
-        changed.sort();
-        added.sort();
+        removed.sort_by(PublicItem::grouping_cmp);
+        changed.sort_by(ChangedPublicItem::grouping_cmp);
+        added.sort_by(PublicItem::grouping_cmp);
 
         Self {
             removed,
diff --git a/public-api/src/intermediate_public_item.rs b/public-api/src/intermediate_public_item.rs
index dd83264..e5caf4c 100644
--- a/public-api/src/intermediate_public_item.rs
+++ b/public-api/src/intermediate_public_item.rs
@@ -42,6 +42,46 @@ impl<'c> NameableItem<'c> {
             sortable_name.push_str(&crate::tokens::tokens_to_string(
                 &context.render_impl(impl_, &[]),
             ));
+
+            // If this is an inherent impl, additionally add the concatenated
+            // names of all associated items to the "name" of the impl. This makes
+            // multiple inherent impls group together, even if they have the
+            // same "name".
+            //
+            // For example, consider this code:
+            //
+            //   pub struct MultipleInherentImpls;
+            //
+            //   impl MultipleInherentImpls {
+            //       pub fn impl_one() {}
+            //   }
+            //
+            //   impl MultipleInherentImpls {
+            //       pub fn impl_two() {}
+            //   }
+            //
+            // In this case, we want to group the two impls together. So
+            // the name of the first impl should be
+            //
+            //   impl MultipleInherentImpls-impl_one
+            //
+            // and the second one
+            //
+            //   impl MultipleInherentImpls-impl_two
+            //
+            if impl_.trait_.is_none() {
+                let mut assoc_item_names: Vec<&str> = impl_
+                    .items
+                    .iter()
+                    .filter_map(|id| context.crate_.index.get(id))
+                    .filter_map(|item| item.name.as_ref())
+                    .map(String::as_str)
+                    .collect();
+                assoc_item_names.sort_unstable();
+
+                sortable_name.push('-');
+                sortable_name.push_str(&assoc_item_names.join("-"));
+            }
         }
 
         sortable_name
diff --git a/public-api/src/lib.rs b/public-api/src/lib.rs
index 4dc416c..917ad21 100644
--- a/public-api/src/lib.rs
+++ b/public-api/src/lib.rs
@@ -229,7 +229,7 @@ impl PublicApi {
         let mut public_api = item_processor::public_api_in_crate(&crate_, options);
 
         if options.sorted {
-            public_api.items.sort();
+            public_api.items.sort_by(PublicItem::grouping_cmp);
         }
 
         Ok(public_api)
diff --git a/public-api/src/public_item.rs b/public-api/src/public_item.rs
index 6f23f16..ccc80ec 100644
--- a/public-api/src/public_item.rs
+++ b/public-api/src/public_item.rs
@@ -1,5 +1,6 @@
 use std::cmp::Ordering;
 use std::fmt::Display;
+use std::hash::Hash;
 
 use crate::intermediate_public_item::IntermediatePublicItem;
 use crate::render::RenderingContext;
@@ -16,7 +17,7 @@ pub(crate) type PublicItemPath = Vec<String>;
 /// of the public API of a crate. Implements [`Display`] so it can be printed. It
 /// also implements [`Ord`], but how items are ordered are not stable yet, and
 /// will change in later versions.
-#[derive(Clone, Eq, PartialEq, Hash)]
+#[derive(Clone)]
 pub struct PublicItem {
     /// Read [`crate::item_processor::sorting_prefix()`] docs for more info
     pub(crate) sortable_path: PublicItemPath,
@@ -40,6 +41,34 @@ impl PublicItem {
     pub fn tokens(&self) -> impl Iterator<Item = &Token> {
         self.tokens.iter()
     }
+
+    /// Special version of [`cmp`](Ord::cmp) that is used to sort public items in a way that
+    /// makes them grouped logically. For example, struct fields will be put
+    /// right after the struct they are part of.
+    #[must_use]
+    pub fn grouping_cmp(&self, other: &Self) -> std::cmp::Ordering {
+        // This will make e.g. struct and struct fields be grouped together.
+        if let Some(ordering) = different_or_none(&self.sortable_path, &other.sortable_path) {
+            return ordering;
+        }
+
+        // Fall back to lexical sorting if the above is not sufficient
+        self.to_string().cmp(&other.to_string())
+    }
+}
+
+impl PartialEq for PublicItem {
+    fn eq(&self, other: &Self) -> bool {
+        self.tokens == other.tokens
+    }
+}
+
+impl Eq for PublicItem {}
+
+impl Hash for PublicItem {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.tokens.hash(state);
+    }
 }
 
 /// We want pretty-printing (`"{:#?}"`) of [`crate::diff::PublicApiDiff`] to print
@@ -58,12 +87,6 @@ impl Display for PublicItem {
     }
 }
 
-impl PartialOrd for PublicItem {
-    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
-        Some(self.cmp(other))
-    }
-}
-
 /// Returns `None` if two items are equal. Otherwise their ordering is returned.
 fn different_or_none<T: Ord>(a: &T, b: &T) -> Option<Ordering> {
     match a.cmp(b) {
@@ -71,15 +94,3 @@ fn different_or_none<T: Ord>(a: &T, b: &T) -> Option<Ordering> {
         c => Some(c),
     }
 }
-
-impl Ord for PublicItem {
-    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
-        // This will make e.g. struct and struct fields be grouped together.
-        if let Some(ordering) = different_or_none(&self.sortable_path, &other.sortable_path) {
-            return ordering;
-        }
-
-        // Fall back to lexical sorting if the above is not sufficient
-        self.to_string().cmp(&other.to_string())
-    }
-}
diff --git a/rustdoc-json/Cargo.toml b/rustdoc-json/Cargo.toml
index f655982..1588c35 100644
--- a/rustdoc-json/Cargo.toml
+++ b/rustdoc-json/Cargo.toml
@@ -22,7 +22,7 @@ expect-test = "1.4.0"
 tempfile = "3.3.0"
 
 [dev-dependencies.predicates]
-version = "2.1.5"
+version = "2.1.4"
 default-features = false
 
 [dev-dependencies.public-api]


Test patch:
diff --git a/cargo-public-api/tests/expected-output/public_api_list.txt b/cargo-public-api/tests/expected-output/public_api_list.txt
index 66c887d..101dfe8 100644
--- a/cargo-public-api/tests/expected-output/public_api_list.txt
+++ b/cargo-public-api/tests/expected-output/public_api_list.txt
@@ -3,15 +3,13 @@ pub mod public_api::diff
 pub struct public_api::diff::ChangedPublicItem
 pub public_api::diff::ChangedPublicItem::new: public_api::PublicItem
 pub public_api::diff::ChangedPublicItem::old: public_api::PublicItem
+impl public_api::diff::ChangedPublicItem
+pub fn public_api::diff::ChangedPublicItem::grouping_cmp(&self, other: &Self) -> core::cmp::Ordering
 impl core::clone::Clone for public_api::diff::ChangedPublicItem
 pub fn public_api::diff::ChangedPublicItem::clone(&self) -> public_api::diff::ChangedPublicItem
 impl core::cmp::Eq for public_api::diff::ChangedPublicItem
-impl core::cmp::Ord for public_api::diff::ChangedPublicItem
-pub fn public_api::diff::ChangedPublicItem::cmp(&self, other: &public_api::diff::ChangedPublicItem) -> core::cmp::Ordering
 impl core::cmp::PartialEq<public_api::diff::ChangedPublicItem> for public_api::diff::ChangedPublicItem
 pub fn public_api::diff::ChangedPublicItem::eq(&self, other: &public_api::diff::ChangedPublicItem) -> bool
-impl core::cmp::PartialOrd<public_api::diff::ChangedPublicItem> for public_api::diff::ChangedPublicItem
-pub fn public_api::diff::ChangedPublicItem::partial_cmp(&self, other: &public_api::diff::ChangedPublicItem) -> core::option::Option<core::cmp::Ordering>
 impl core::fmt::Debug for public_api::diff::ChangedPublicItem
 pub fn public_api::diff::ChangedPublicItem::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
 impl core::marker::StructuralEq for public_api::diff::ChangedPublicItem
@@ -104,23 +102,18 @@ impl core::fmt::Debug for public_api::PublicApi
 pub fn public_api::PublicApi::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
 pub struct public_api::PublicItem
 impl public_api::PublicItem
+pub fn public_api::PublicItem::grouping_cmp(&self, other: &Self) -> core::cmp::Ordering
 pub fn public_api::PublicItem::tokens(&self) -> impl core::iter::traits::iterator::Iterator<Item = &public_api::tokens::Token>
-impl core::cmp::Ord for public_api::PublicItem
-pub fn public_api::PublicItem::cmp(&self, other: &Self) -> core::cmp::Ordering
-impl core::cmp::PartialOrd<public_api::PublicItem> for public_api::PublicItem
-pub fn public_api::PublicItem::partial_cmp(&self, other: &Self) -> core::option::Option<core::cmp::Ordering>
+impl core::cmp::Eq for public_api::PublicItem
+impl core::cmp::PartialEq<public_api::PublicItem> for public_api::PublicItem
+pub fn public_api::PublicItem::eq(&self, other: &Self) -> bool
 impl core::fmt::Debug for public_api::PublicItem
 pub fn public_api::PublicItem::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
 impl core::fmt::Display for public_api::PublicItem
 pub fn public_api::PublicItem::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+impl core::hash::Hash for public_api::PublicItem
+pub fn public_api::PublicItem::hash<H: core::hash::Hasher>(&self, state: &mut H)
 impl core::clone::Clone for public_api::PublicItem
 pub fn public_api::PublicItem::clone(&self) -> public_api::PublicItem
-impl core::cmp::Eq for public_api::PublicItem
-impl core::cmp::PartialEq<public_api::PublicItem> for public_api::PublicItem
-pub fn public_api::PublicItem::eq(&self, other: &public_api::PublicItem) -> bool
-impl core::hash::Hash for public_api::PublicItem
-pub fn public_api::PublicItem::hash<__H: core::hash::Hasher>(&self, state: &mut __H) -> ()
-impl core::marker::StructuralEq for public_api::PublicItem
-impl core::marker::StructuralPartialEq for public_api::PublicItem
 pub const public_api::MINIMUM_RUSTDOC_JSON_VERSION: &str
 pub type public_api::Result<T> = core::result::Result<T, public_api::Error>
diff --git a/public-api/tests/common/mod.rs b/public-api/tests/common/mod.rs
index 774830c..6d85c6e 100644
--- a/public-api/tests/common/mod.rs
+++ b/public-api/tests/common/mod.rs
@@ -1,3 +1,5 @@
+#![allow(unused)] // Used from many crates, but not all crates use all functions.
+
 use std::path::{Path, PathBuf};
 
 /// Builds rustdoc JSON for the given test crate.
@@ -5,12 +7,25 @@ use std::path::{Path, PathBuf};
 /// Output of child processes are not captured by the Rust test framework (see
 /// <https://users.rust-lang.org/t/cargo-doesnt-capture-stderr-in-tests/67045/4>),
 /// so build quietly to avoid noisy test output.
-pub fn rustdoc_json_path_for_crate(test_crate: &str, target_dir: impl AsRef<Path>) -> PathBuf {
+pub fn rustdoc_json_path_for_crate(
+    test_crate: impl AsRef<Path>,
+    target_dir: impl AsRef<Path>,
+) -> PathBuf {
+    let mut manifest_path = test_crate.as_ref().to_path_buf();
+    manifest_path.push("Cargo.toml");
+
     rustdoc_json::Builder::default()
-        .manifest_path(&format!("{}/Cargo.toml", test_crate))
+        .manifest_path(&manifest_path)
         .toolchain("nightly".to_owned())
         .target_dir(target_dir)
         .quiet(true)
         .build()
         .unwrap()
 }
+
+/// Builds rustdoc JSON for the given temporary test crate. A temporary test
+/// crate is a crate set up in a temporary directory, so that the target dir and
+/// root dir can be the same while still allowing tests to run concurrently.
+pub fn rustdoc_json_path_for_temp_crate(temp_crate: impl AsRef<Path>) -> PathBuf {
+    rustdoc_json_path_for_crate(temp_crate.as_ref(), temp_crate.as_ref())
+}
diff --git a/public-api/tests/expected-output/comprehensive_api.txt b/public-api/tests/expected-output/comprehensive_api.txt
index 0cdd93f..d79b53d 100644
--- a/public-api/tests/expected-output/comprehensive_api.txt
+++ b/public-api/tests/expected-output/comprehensive_api.txt
@@ -149,8 +149,8 @@ pub fn comprehensive_api::structs::Plain::s1(self)
 pub fn comprehensive_api::structs::Plain::s2(&self)
 pub fn comprehensive_api::structs::Plain::s3(&mut self)
 impl<'a> comprehensive_api::structs::Plain
-impl<'a> comprehensive_api::structs::Plain
 pub fn comprehensive_api::structs::Plain::s4(&'a self)
+impl<'a> comprehensive_api::structs::Plain
 pub fn comprehensive_api::structs::Plain::s5(&'a self)
 pub struct comprehensive_api::structs::PrivateField
 pub struct comprehensive_api::structs::TupleStructDouble(pub usize, pub bool)
@@ -169,8 +169,8 @@ pub struct comprehensive_api::structs::WithLifetimeAndGenericParam<'a, T>
 pub comprehensive_api::structs::WithLifetimeAndGenericParam::t: T
 pub comprehensive_api::structs::WithLifetimeAndGenericParam::unit_ref: &'a comprehensive_api::structs::Unit
 impl<'b, T> comprehensive_api::structs::WithLifetimeAndGenericParam<'b, T>
-impl<'b, T> comprehensive_api::structs::WithLifetimeAndGenericParam<'b, T>
 pub fn comprehensive_api::structs::WithLifetimeAndGenericParam::new_any(unit_ref: &'b comprehensive_api::structs::Unit, t: T) -> Self
+impl<'b, T> comprehensive_api::structs::WithLifetimeAndGenericParam<'b, T>
 pub fn comprehensive_api::structs::WithLifetimeAndGenericParam::new_any_duplicate(unit_ref: &'b comprehensive_api::structs::Unit, t: T) -> Self
 impl<'b> comprehensive_api::structs::WithLifetimeAndGenericParam<'b, alloc::string::String>
 pub fn comprehensive_api::structs::WithLifetimeAndGenericParam::new(unit_ref: &'b comprehensive_api::structs::Unit, t: alloc::string::String) -> Self
@@ -233,8 +233,8 @@ pub fn comprehensive_api::Plain::s1(self)
 pub fn comprehensive_api::Plain::s2(&self)
 pub fn comprehensive_api::Plain::s3(&mut self)
 impl<'a> comprehensive_api::structs::Plain
-impl<'a> comprehensive_api::structs::Plain
 pub fn comprehensive_api::Plain::s4(&'a self)
+impl<'a> comprehensive_api::structs::Plain
 pub fn comprehensive_api::Plain::s5(&'a self)
 pub struct comprehensive_api::RenamedPlain
 pub comprehensive_api::RenamedPlain::x: usize
@@ -245,7 +245,7 @@ pub fn comprehensive_api::RenamedPlain::s1(self)
 pub fn comprehensive_api::RenamedPlain::s2(&self)
 pub fn comprehensive_api::RenamedPlain::s3(&mut self)
 impl<'a> comprehensive_api::structs::Plain
-impl<'a> comprehensive_api::structs::Plain
 pub fn comprehensive_api::RenamedPlain::s4(&'a self)
+impl<'a> comprehensive_api::structs::Plain
 pub fn comprehensive_api::RenamedPlain::s5(&'a self)
 pub struct comprehensive_api::StructInPrivateMod
diff --git a/public-api/tests/expected-output/diff_move_item_between_inherent_impls.txt b/public-api/tests/expected-output/diff_move_item_between_inherent_impls.txt
new file mode 100644
index 0000000..386f7d4
--- /dev/null
+++ b/public-api/tests/expected-output/diff_move_item_between_inherent_impls.txt
@@ -0,0 +1,5 @@
+PublicApiDiff {
+    removed: [],
+    changed: [],
+    added: [],
+}
diff --git a/public-api/tests/public-api-lib-tests.rs b/public-api/tests/public-api-lib-tests.rs
index 92230f7..11afe99 100644
--- a/public-api/tests/public-api-lib-tests.rs
+++ b/public-api/tests/public-api-lib-tests.rs
@@ -1,15 +1,18 @@
 // deny in CI, only warn here
 #![warn(clippy::all, clippy::pedantic)]
 
-use std::path::Path;
+use std::{
+    fs,
+    path::{Path, PathBuf},
+};
 
 use expect_test::expect_file;
 use public_api::{Error, Options, PublicApi};
 
-use tempfile::tempdir;
+use tempfile::{tempdir, TempDir};
 
 mod common;
-use common::rustdoc_json_path_for_crate;
+use common::{rustdoc_json_path_for_crate, rustdoc_json_path_for_temp_crate};
 
 #[test]
 fn public_api() -> Result<(), Box<dyn std::error::Error>> {
@@ -108,6 +111,41 @@ fn no_diff() {
     );
 }
 
+#[test]
+fn diff_empty_when_item_moved_between_inherent_impls() {
+    let v1 = rustdoc_json_for_lib(
+        r#"
+pub struct Foo;
+impl Foo {
+    pub fn f1() {}
+    pub fn moved() {}
+}
+impl Foo {
+    pub fn f2() {}
+}
+    "#,
+    );
+
+    let v2 = rustdoc_json_for_lib(
+        r#"
+pub struct Foo;
+impl Foo {
+    pub fn f1() {}
+}
+impl Foo {
+    pub fn f2() {}
+    pub fn moved() {}
+}
+    "#,
+    );
+
+    assert_public_api_diff(
+        &v1.json_path,
+        &v2.json_path,
+        "./expected-output/diff_move_item_between_inherent_impls.txt",
+    );
+}
+
 #[test]
 fn diff_with_removed_items() {
     // Create independent build dirs so all tests can run in parallel
@@ -143,6 +181,22 @@ fn comprehensive_api_proc_macro() {
     );
 }
 
+/// Test that `debug_sorting` does not result in stack overflow because of
+/// recursion. This can quite easily happen unless we test for it continuously.
+/// We don't care what the exact output is, just that we don't crash.
+#[test]
+fn comprehensive_api_debug_sorting_no_stack_overflow() {
+    // Create independent build dir so all tests can run in parallel
+    let build_dir = tempdir().unwrap();
+
+    let mut options = Options::default();
+    options.debug_sorting = true;
+    let rustdoc_json = rustdoc_json_path_for_crate("../test-apis/comprehensive_api", &build_dir);
+    let _api = PublicApi::from_rustdoc_json(rustdoc_json, options)
+        .unwrap()
+        .to_string();
+}
+
 #[test]
 fn invalid_json() {
     let result = PublicApi::from_rustdoc_json_str("}}}}}}}}}", Options::default());
@@ -159,6 +213,41 @@ fn options() {
     let _ = options.clone();
 }
 
+struct LibWithJson {
+    json_path: PathBuf,
+
+    /// Keep the tempdir alive so the json file doesn't get deleted
+    _root: TempDir,
+}
+
+fn rustdoc_json_for_lib(lib: &str) -> LibWithJson {
+    let root = tempdir().unwrap();
+
+    let write = |file: &str, content: &str| {
+        let file_path = root.path().join(file);
+        fs::write(file_path, content).unwrap();
+    };
+
+    write(
+        "Cargo.toml",
+        "\
+        [package]\n\
+        name = \"lib\"\n\
+        version = \"0.1.0\"\n\
+        edition = \"2021\"\n\
+        [lib]\n\
+        path = \"lib.rs\"\n\
+        ",
+    );
+
+    write("lib.rs", lib);
+
+    LibWithJson {
+        json_path: rustdoc_json_path_for_temp_crate(&root),
+        _root: root,
+    }
+}
+
 fn assert_public_api_diff(
     old_json: impl AsRef<Path>,
     new_json: impl AsRef<Path>,