Allow user to specify semver compatibility logic for suggested updates.

Test: treehugger, ran crate updater.
Change-Id: Ide337e60bdae7c835242477a36d939f6ec0da2bf
diff --git a/tools/external_crates/crate_tool/src/lib.rs b/tools/external_crates/crate_tool/src/lib.rs
index e0ca11f..d6f3238 100644
--- a/tools/external_crates/crate_tool/src/lib.rs
+++ b/tools/external_crates/crate_tool/src/lib.rs
@@ -35,6 +35,7 @@
 
 pub use self::android_bp::maybe_build_cargo_embargo;
 pub use self::managed_repo::ManagedRepo;
+pub use self::upgradable::SemverCompatibilityRule;
 
 #[derive(Error, Debug)]
 pub enum CrateError {
diff --git a/tools/external_crates/crate_tool/src/main.rs b/tools/external_crates/crate_tool/src/main.rs
index 5f8bb1c..bd9a617 100644
--- a/tools/external_crates/crate_tool/src/main.rs
+++ b/tools/external_crates/crate_tool/src/main.rs
@@ -19,7 +19,9 @@
 
 use anyhow::Result;
 use clap::{Args, Parser, Subcommand};
-use crate_tool::{default_repo_root, maybe_build_cargo_embargo, ManagedRepo};
+use crate_tool::{
+    default_repo_root, maybe_build_cargo_embargo, ManagedRepo, SemverCompatibilityRule,
+};
 use rooted_path::RootedPath;
 use semver::Version;
 
@@ -114,6 +116,10 @@
         /// Don't exclude crates that have patches.
         #[arg(long, default_value_t = false)]
         patches: bool,
+
+        /// How strict to be about enforcing semver compatibility.
+        #[arg(long, value_enum, default_value_t = SemverCompatibilityRule::Loose)]
+        semver_compatibility: SemverCompatibilityRule,
     },
     /// Update a crate to the specified version.
     Update {
@@ -238,7 +244,9 @@
         }
         Cmd::UpdatableCrates {} => managed_repo.updatable_crates(),
         Cmd::AnalyzeUpdates { crate_name } => managed_repo.analyze_updates(crate_name),
-        Cmd::SuggestUpdates { patches } => managed_repo.suggest_updates(patches).map(|_x| ()),
+        Cmd::SuggestUpdates { patches, semver_compatibility } => {
+            managed_repo.suggest_updates(patches, semver_compatibility).map(|_x| ())
+        }
         Cmd::Update { crate_name, version } => managed_repo.update(crate_name, version),
         Cmd::Init {} => managed_repo.init(),
         Cmd::TestMapping { crates } => {
diff --git a/tools/external_crates/crate_tool/src/managed_repo.rs b/tools/external_crates/crate_tool/src/managed_repo.rs
index cebbde9..6935988 100644
--- a/tools/external_crates/crate_tool/src/managed_repo.rs
+++ b/tools/external_crates/crate_tool/src/managed_repo.rs
@@ -42,7 +42,7 @@
     license::{most_restrictive_type, update_module_license_files},
     managed_crate::ManagedCrate,
     pseudo_crate::{CargoVendorDirty, PseudoCrate},
-    upgradable::{IsUpgradableTo, MatchesRelaxed},
+    upgradable::{IsUpgradableTo, MatchesWithCompatibilityRule, SemverCompatibilityRule},
     SuccessOrError,
 };
 
@@ -416,8 +416,12 @@
                     continue;
                 }
                 let versions = cc.get_versions(dep.crate_name()).collect::<Vec<_>>();
-                let has_matching_version =
-                    versions.iter().any(|(nv, _)| req.matches_relaxed(nv.version()));
+                let has_matching_version = versions.iter().any(|(nv, _)| {
+                    req.matches_with_compatibility_rule(
+                        nv.version(),
+                        SemverCompatibilityRule::Loose,
+                    )
+                });
                 if !has_matching_version {
                     found_problems = true;
                 }
@@ -430,7 +434,14 @@
                             "  Dep {} {} is {}satisfied by v{} at {}",
                             dep.crate_name(),
                             dep.requirement(),
-                            if req.matches_relaxed(dep_crate.version()) { "" } else { "not " },
+                            if req.matches_with_compatibility_rule(
+                                dep_crate.version(),
+                                SemverCompatibilityRule::Loose
+                            ) {
+                                ""
+                            } else {
+                                "not "
+                            },
                             dep_crate.version(),
                             dep_crate.path()
                         );
@@ -698,9 +709,15 @@
             println!("Version {}", version.version());
             let mut found_problems = false;
             let parsed_version = semver::Version::parse(version.version())?;
-            if !krate.android_version().is_upgradable_to(&parsed_version) {
+            if !krate
+                .android_version()
+                .is_upgradable_to(&parsed_version, SemverCompatibilityRule::Strict)
+            {
                 found_problems = true;
-                if !krate.android_version().is_upgradable_to_relaxed(&parsed_version) {
+                if !krate
+                    .android_version()
+                    .is_upgradable_to(&parsed_version, SemverCompatibilityRule::Loose)
+                {
                     println!("  Not semver-compatible, even by relaxed standards");
                 } else {
                     println!("  Semver-compatible, but only by relaxed standards since major version is 0");
@@ -731,7 +748,10 @@
                     }
                 }
                 for (_, dep_crate) in cc.get_versions(dep.crate_name()) {
-                    if !req.matches_relaxed(dep_crate.version()) {
+                    if !req.matches_with_compatibility_rule(
+                        dep_crate.version(),
+                        SemverCompatibilityRule::Loose,
+                    ) {
                         found_problems = true;
                         println!(
                             "  Dep {} {} is not satisfied by v{} at {}",
@@ -753,7 +773,11 @@
 
         Ok(())
     }
-    pub fn suggest_updates(&self, consider_patched_crates: bool) -> Result<Vec<(String, String)>> {
+    pub fn suggest_updates(
+        &self,
+        consider_patched_crates: bool,
+        semver_compatibility: SemverCompatibilityRule,
+    ) -> Result<Vec<(String, String)>> {
         let mut suggestions = Vec::new();
         let mut managed_crates = self.new_cc();
         managed_crates.add_from(self.managed_dir().rel())?;
@@ -786,7 +810,7 @@
 
             for version in cio_crate.versions_gt(krate.version()).rev() {
                 let parsed_version = semver::Version::parse(version.version())?;
-                if !krate.version().is_upgradable_to_relaxed(&parsed_version) {
+                if !krate.version().is_upgradable_to(&parsed_version, semver_compatibility) {
                     continue;
                 }
                 if !version.android_deps_with_version_reqs().any(|(dep, req)| {
@@ -799,7 +823,10 @@
                         &legacy_crates
                     };
                     for (_, dep_crate) in cc.get_versions(dep.crate_name()) {
-                        if req.matches_relaxed(dep_crate.version()) {
+                        if req.matches_with_compatibility_rule(
+                            dep_crate.version(),
+                            SemverCompatibilityRule::Loose,
+                        ) {
                             return false;
                         }
                     }
diff --git a/tools/external_crates/crate_tool/src/upgradable.rs b/tools/external_crates/crate_tool/src/upgradable.rs
index 1cf441c..2803bfb 100644
--- a/tools/external_crates/crate_tool/src/upgradable.rs
+++ b/tools/external_crates/crate_tool/src/upgradable.rs
@@ -12,45 +12,76 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+use clap::ValueEnum;
 use semver::{Version, VersionReq};
 
+/// How strictly to enforce semver compatibility.
+#[derive(Copy, Clone, ValueEnum)]
+pub enum SemverCompatibilityRule {
+    /// Ignore semantic version. Consider any two versions compatible.
+    Ignore,
+    /// Consider 0.x and 0.y to be compatible, but otherwise follow standard rules.
+    Loose,
+    /// Follow standard semantic version rules, under which 0.x and 0.y are incompatible.
+    Strict,
+}
 /// A trait for determining semver compatibility.
 pub trait IsUpgradableTo {
-    /// Returns true if the object version is semver-compatible with 'other'.
-    fn is_upgradable_to(&self, other: &Version) -> bool;
-    /// Returns true if the object version is semver-compatible with 'other', or if
-    /// both have a major version of 0 and the other version is greater.
-    fn is_upgradable_to_relaxed(&self, other: &Version) -> bool;
+    /// Returns true if the object version is upgradable to 'other', according to
+    /// the specified semantic version compatibility strictness.
+    fn is_upgradable_to(
+        &self,
+        other: &Version,
+        semver_compatibility: SemverCompatibilityRule,
+    ) -> bool;
 }
 
 impl IsUpgradableTo for semver::Version {
-    fn is_upgradable_to(&self, other: &Version) -> bool {
-        VersionReq::parse(&self.to_string()).is_ok_and(|req| req.matches(other))
-    }
-    fn is_upgradable_to_relaxed(&self, other: &Version) -> bool {
-        VersionReq::parse(&self.to_string()).is_ok_and(|req| req.matches_relaxed(other))
+    fn is_upgradable_to(
+        &self,
+        other: &Version,
+        semver_compatibility: SemverCompatibilityRule,
+    ) -> bool {
+        other > self
+            && VersionReq::parse(&self.to_string())
+                .is_ok_and(|req| req.matches_with_compatibility_rule(other, semver_compatibility))
     }
 }
 
-/// A trait for relaxed semver compatibility. Major versions of 0 are treated as if they were non-zero.
-pub trait MatchesRelaxed {
-    /// Returns true if the version matches the req, but treats
-    /// major version of zero as if it were non-zero.
-    fn matches_relaxed(&self, version: &Version) -> bool;
+/// A trait for custom semver compatibility logic, allowing it to be ignored or relaxed.
+pub trait MatchesWithCompatibilityRule {
+    /// Returns true if the version matches the req, according to the
+    /// custom compatibility requirements of 'semver_compatibility'.
+    fn matches_with_compatibility_rule(
+        &self,
+        version: &Version,
+        semver_compatibility: SemverCompatibilityRule,
+    ) -> bool;
 }
-impl MatchesRelaxed for VersionReq {
-    fn matches_relaxed(&self, version: &Version) -> bool {
-        if self.matches(version) {
-            return true;
+impl MatchesWithCompatibilityRule for VersionReq {
+    fn matches_with_compatibility_rule(
+        &self,
+        version: &Version,
+        semver_compatibility: SemverCompatibilityRule,
+    ) -> bool {
+        match semver_compatibility {
+            SemverCompatibilityRule::Ignore => true,
+            SemverCompatibilityRule::Loose => {
+                if self.comparators.len() == 1
+                    && self.comparators[0].major == 0
+                    && version.major == 0
+                {
+                    let mut fake_v = version.clone();
+                    fake_v.major = 1;
+                    let mut fake_req = self.clone();
+                    fake_req.comparators[0].major = 1;
+                    fake_req.matches(&fake_v)
+                } else {
+                    self.matches(version)
+                }
+            }
+            SemverCompatibilityRule::Strict => self.matches(version),
         }
-        if self.comparators.len() == 1 && self.comparators[0].major == 0 && version.major == 0 {
-            let mut fake_v = version.clone();
-            fake_v.major = 1;
-            let mut fake_req = self.clone();
-            fake_req.comparators[0].major = 1;
-            return fake_req.matches(&fake_v);
-        }
-        false
     }
 }
 
@@ -68,18 +99,58 @@
         let major = Version::parse("3.0.0")?;
         let older = Version::parse("2.3.3")?;
 
-        // All have same behavior for is_upgradable_to_relaxed
-        assert!(version.is_upgradable_to(&patch), "Patch update");
-        assert!(version.is_upgradable_to_relaxed(&patch), "Patch update");
+        // All have same behavior for SemverCompatibility::LOOSE.
+        assert!(
+            version.is_upgradable_to(&patch, SemverCompatibilityRule::Strict),
+            "Patch update, strict"
+        );
+        assert!(
+            version.is_upgradable_to(&patch, SemverCompatibilityRule::Loose),
+            "Patch update, loose"
+        );
+        assert!(
+            version.is_upgradable_to(&patch, SemverCompatibilityRule::Ignore),
+            "Patch update, ignore"
+        );
 
-        assert!(version.is_upgradable_to(&minor), "Minor version update");
-        assert!(version.is_upgradable_to_relaxed(&minor), "Minor version update");
+        assert!(
+            version.is_upgradable_to(&minor, SemverCompatibilityRule::Strict),
+            "Minor version update, strict"
+        );
+        assert!(
+            version.is_upgradable_to(&minor, SemverCompatibilityRule::Loose),
+            "Minor version update, loose"
+        );
+        assert!(
+            version.is_upgradable_to(&minor, SemverCompatibilityRule::Ignore),
+            "Minor version update, ignore"
+        );
 
-        assert!(!version.is_upgradable_to(&major), "Incompatible (major version) update");
-        assert!(!version.is_upgradable_to_relaxed(&major), "Incompatible (major version) update");
+        assert!(
+            !version.is_upgradable_to(&major, SemverCompatibilityRule::Strict),
+            "Incompatible (major version) update, strict"
+        );
+        assert!(
+            !version.is_upgradable_to(&major, SemverCompatibilityRule::Loose),
+            "Incompatible (major version) update, loose"
+        );
+        assert!(
+            version.is_upgradable_to(&major, SemverCompatibilityRule::Ignore),
+            "Incompatible (major version) update, ignore"
+        );
 
-        assert!(!version.is_upgradable_to(&older), "Downgrade");
-        assert!(!version.is_upgradable_to_relaxed(&older), "Downgrade");
+        assert!(
+            !version.is_upgradable_to(&older, SemverCompatibilityRule::Strict),
+            "Downgrade, strict"
+        );
+        assert!(
+            !version.is_upgradable_to(&older, SemverCompatibilityRule::Loose),
+            "Downgrade, loose"
+        );
+        assert!(
+            !version.is_upgradable_to(&older, SemverCompatibilityRule::Ignore),
+            "Downgrade, ignore"
+        );
 
         Ok(())
     }
@@ -92,18 +163,58 @@
         let major = Version::parse("1.0.0")?;
         let older = Version::parse("0.3.3")?;
 
-        assert!(version.is_upgradable_to(&patch), "Patch update");
-        assert!(version.is_upgradable_to_relaxed(&patch), "Patch update");
+        assert!(
+            version.is_upgradable_to(&patch, SemverCompatibilityRule::Strict),
+            "Patch update, strict"
+        );
+        assert!(
+            version.is_upgradable_to(&patch, SemverCompatibilityRule::Loose),
+            "Patch update, loose"
+        );
+        assert!(
+            version.is_upgradable_to(&patch, SemverCompatibilityRule::Ignore),
+            "Patch update, ignore"
+        );
 
         // Different behavior for minor version changes.
-        assert!(!version.is_upgradable_to(&minor), "Minor version update");
-        assert!(version.is_upgradable_to_relaxed(&minor), "Minor version update");
+        assert!(
+            !version.is_upgradable_to(&minor, SemverCompatibilityRule::Strict),
+            "Minor version update, strict"
+        );
+        assert!(
+            version.is_upgradable_to(&minor, SemverCompatibilityRule::Loose),
+            "Minor version update, loose"
+        );
+        assert!(
+            version.is_upgradable_to(&minor, SemverCompatibilityRule::Ignore),
+            "Minor version update, ignore"
+        );
 
-        assert!(!version.is_upgradable_to(&major), "Incompatible (major version) update");
-        assert!(!version.is_upgradable_to_relaxed(&major), "Incompatible (major version) update");
+        assert!(
+            !version.is_upgradable_to(&major, SemverCompatibilityRule::Strict),
+            "Incompatible (major version) update, strict"
+        );
+        assert!(
+            !version.is_upgradable_to(&major, SemverCompatibilityRule::Loose),
+            "Incompatible (major version) update, loose"
+        );
+        assert!(
+            version.is_upgradable_to(&major, SemverCompatibilityRule::Ignore),
+            "Incompatible (major version) update, ignore"
+        );
 
-        assert!(!version.is_upgradable_to(&older), "Downgrade");
-        assert!(!version.is_upgradable_to_relaxed(&older), "Downgrade");
+        assert!(
+            !version.is_upgradable_to(&older, SemverCompatibilityRule::Strict),
+            "Downgrade, strict"
+        );
+        assert!(
+            !version.is_upgradable_to(&older, SemverCompatibilityRule::Loose),
+            "Downgrade, loose"
+        );
+        assert!(
+            !version.is_upgradable_to(&older, SemverCompatibilityRule::Ignore),
+            "Downgrade, ignore"
+        );
 
         Ok(())
     }