diff --git a/internal/getproviders/types.go b/internal/getproviders/types.go index 8a5b382e4..c71fc5893 100644 --- a/internal/getproviders/types.go +++ b/internal/getproviders/types.go @@ -407,7 +407,18 @@ func VersionConstraintsString(spec VersionConstraints) string { // and sort them into a consistent order. sels := make(map[constraints.SelectionSpec]struct{}) for _, sel := range spec { - sels[sel] = struct{}{} + // The parser allows writing abbreviated version (such as 2) which + // end up being represented in memory with trailing unconstrained parts + // (for example 2.*.*). For the purpose of serialization with Ruby + // style syntax, these unconstrained parts can all be represented as 0 + // with no loss of meaning, so we make that conversion here. Doing so + // allows us to deduplicate equivalent constraints, such as >= 2.0 and + // >= 2.0.0. + normalizedSel := constraints.SelectionSpec{ + Operator: sel.Operator, + Boundary: sel.Boundary.ConstrainToZero(), + } + sels[normalizedSel] = struct{}{} } selsOrder := make([]constraints.SelectionSpec, 0, len(sels)) for sel := range sels { @@ -450,34 +461,26 @@ func VersionConstraintsString(spec VersionConstraints) string { b.WriteString("??? ") } - // The parser allows writing abbreviated version (such as 2) which - // end up being represented in memory with trailing unconstrained parts - // (for example 2.*.*). For the purpose of serialization with Ruby - // style syntax, these unconstrained parts can all be represented as 0 - // with no loss of meaning, so we make that conversion here. - // - // This is possible because we use a different constraint operator to - // distinguish between the two types of pessimistic constraint: - // minor-only and patch-only. For minor-only constraints, we always - // want to display only the major and minor version components, so we - // special-case that operator below. + // We use a different constraint operator to distinguish between the + // two types of pessimistic constraint: minor-only and patch-only. For + // minor-only constraints, we always want to display only the major and + // minor version components, so we special-case that operator below. // // One final edge case is a minor-only constraint specified with only // the major version, such as ~> 2. We treat this the same as ~> 2.0, // because a major-only pessimistic constraint does not exist: it is // logically identical to >= 2.0.0. - boundary := sel.Boundary.ConstrainToZero() if sel.Operator == constraints.OpGreaterThanOrEqualMinorOnly { // The minor-pessimistic syntax uses only two version components. - fmt.Fprintf(&b, "%s.%s", boundary.Major, boundary.Minor) + fmt.Fprintf(&b, "%s.%s", sel.Boundary.Major, sel.Boundary.Minor) } else { - fmt.Fprintf(&b, "%s.%s.%s", boundary.Major, boundary.Minor, boundary.Patch) + fmt.Fprintf(&b, "%s.%s.%s", sel.Boundary.Major, sel.Boundary.Minor, sel.Boundary.Patch) } if sel.Boundary.Prerelease != "" { - b.WriteString("-" + boundary.Prerelease) + b.WriteString("-" + sel.Boundary.Prerelease) } if sel.Boundary.Metadata != "" { - b.WriteString("+" + boundary.Metadata) + b.WriteString("+" + sel.Boundary.Metadata) } } return b.String() diff --git a/internal/getproviders/types_test.go b/internal/getproviders/types_test.go index fb8c7669c..b12cc2155 100644 --- a/internal/getproviders/types_test.go +++ b/internal/getproviders/types_test.go @@ -53,6 +53,10 @@ func TestVersionConstraintsString(t *testing.T) { MustParseVersionConstraints(">= 1.2.3, 1.2.3, ~> 1.2, 1.2.3"), "~> 1.2, >= 1.2.3, 1.2.3", }, + "equivalent duplicates removed": { + MustParseVersionConstraints(">= 2.68, >= 2.68.0"), + ">= 2.68.0", + }, "consistent ordering, exhaustive": { // This weird jumble is just to exercise the different sort // ordering codepaths. Hopefully nothing quite this horrific