From 9e82906db577b662459c27f90cd8ca0cb19d11f7 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Tue, 12 May 2026 12:36:44 +0100 Subject: [PATCH] vers: HighestSatisfying helper for resolver-style version picks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The common shape of a package-manager resolver: fetch the list of available versions from the registry, then pick the highest one that still satisfies the user's manifest constraint. Callers all wrote the same loop: parsed, _ := vers.ParseNative(constraint, scheme) var best string for _, v := range versions { if !parsed.Contains(v) { continue } if best == "" || vers.CompareWithScheme(v, best, scheme) > 0 { best = v } } Surfaces it as one call: best, err := vers.HighestSatisfying(versions, constraint, scheme) Empty scheme parses constraint as a vers URI; non-empty scheme parses it as native syntax (the same dispatch Satisfies uses). Versions that fail to parse are skipped, not fatal — pulling messy registry data through HighestSatisfying doesn't need a pre-filter pass. --- highest_satisfying_test.go | 74 ++++++++++++++++++++++++++++++++++++++ vers.go | 35 ++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 highest_satisfying_test.go diff --git a/highest_satisfying_test.go b/highest_satisfying_test.go new file mode 100644 index 0000000..c6e225f --- /dev/null +++ b/highest_satisfying_test.go @@ -0,0 +1,74 @@ +package vers + +import "testing" + +func TestHighestSatisfying_Npm(t *testing.T) { + versions := []string{"1.0.0", "1.5.0", "2.0.0", "2.5.0", "3.0.0"} + cases := []struct { + constraint string + want string + }{ + {"^1.0", "1.5.0"}, + {"^2.0.0", "2.5.0"}, + {"~2.0.0", "2.0.0"}, + {">=1.0.0 <2.0.0", "1.5.0"}, + {"^4.0.0", ""}, // no satisfying version + } + for _, tc := range cases { + got, err := HighestSatisfying(versions, tc.constraint, "npm") + if err != nil { + t.Errorf("HighestSatisfying(%q): %v", tc.constraint, err) + continue + } + if got != tc.want { + t.Errorf("HighestSatisfying(%q) = %q, want %q", tc.constraint, got, tc.want) + } + } +} + +func TestHighestSatisfying_OrderIndependent(t *testing.T) { + // Same set, different order — picks the same highest. + a := []string{"1.0.0", "2.0.0", "1.5.0"} + b := []string{"2.0.0", "1.5.0", "1.0.0"} + gotA, _ := HighestSatisfying(a, "^1.0", "npm") + gotB, _ := HighestSatisfying(b, "^1.0", "npm") + if gotA != "1.5.0" || gotB != "1.5.0" { + t.Errorf("order mismatch: a=%q b=%q", gotA, gotB) + } +} + +func TestHighestSatisfying_SkipsInvalidVersions(t *testing.T) { + // Garbage versions should be skipped, not stop the walk. + versions := []string{"not-a-version", "1.0.0", "also-bad", "1.5.0"} + got, err := HighestSatisfying(versions, "^1.0", "npm") + if err != nil { + t.Fatal(err) + } + if got != "1.5.0" { + t.Errorf("got %q, want 1.5.0", got) + } +} + +func TestHighestSatisfying_VersURI(t *testing.T) { + // Empty scheme → constraint is a vers URI rather than native syntax. + got, err := HighestSatisfying( + []string{"1.0.0", "1.5.0", "2.0.0"}, + "vers:npm/>=1.0.0|<2.0.0", + "") + if err != nil { + t.Fatal(err) + } + if got != "1.5.0" { + t.Errorf("got %q, want 1.5.0", got) + } +} + +func TestHighestSatisfying_Empty(t *testing.T) { + got, err := HighestSatisfying(nil, "^1.0", "npm") + if err != nil { + t.Fatal(err) + } + if got != "" { + t.Errorf("got %q, want empty", got) + } +} diff --git a/vers.go b/vers.go index 25d4404..4044258 100644 --- a/vers.go +++ b/vers.go @@ -77,6 +77,41 @@ func Compare(a, b string) int { return CompareVersions(a, b) } +// HighestSatisfying returns the highest version in versions that +// satisfies constraint under the given scheme. Versions that fail to +// parse are skipped. Returns ("", nil) when no version in the list +// satisfies the constraint — a non-nil error is reserved for a +// constraint that itself fails to parse. +// +// Common shape for package-manager resolvers: fetch the list of +// available versions from the registry, then pick the highest one +// that still satisfies the user's manifest constraint. +// +// If scheme is empty, constraint is parsed as a vers URI. +func HighestSatisfying(versions []string, constraint, scheme string) (string, error) { + var r *Range + var err error + if scheme == "" { + r, err = Parse(constraint) + } else { + r, err = ParseNative(constraint, scheme) + } + if err != nil { + return "", err + } + + var best string + for _, v := range versions { + if !r.Contains(v) { + continue + } + if best == "" || CompareWithScheme(v, best, scheme) > 0 { + best = v + } + } + return best, nil +} + // Valid checks if a version string is valid. func Valid(version string) bool { _, err := ParseVersion(version)