diff --git a/conformance_test.go b/conformance_test.go index 8708154..5158345 100644 --- a/conformance_test.go +++ b/conformance_test.go @@ -136,6 +136,7 @@ func TestConformance_Containment(t *testing.T) { } } +//nolint:gocognit func TestConformance_VersionComparison(t *testing.T) { files := []string{ "nuget_version_cmp_test.json", @@ -189,20 +190,19 @@ func TestConformance_VersionComparison(t *testing.T) { t.Run("cmp_"+v1+"_"+v2, func(t *testing.T) { cmp := CompareWithScheme(v1, v2, input.InputScheme) - // expected[0] should be less than expected[1] - // Use comparison to determine which version v1 matches (handles case normalization) v1MatchesFirst := CompareWithScheme(v1, expected[0], input.InputScheme) == 0 - if expected[0] == expected[1] || CompareWithScheme(expected[0], expected[1], input.InputScheme) == 0 { + versionsEqual := expected[0] == expected[1] || CompareWithScheme(expected[0], expected[1], input.InputScheme) == 0 + + switch { + case versionsEqual: if cmp != 0 { t.Errorf("CompareWithScheme(%q, %q, %q) = %d, want 0 (equal versions)", v1, v2, input.InputScheme, cmp) } - } else if v1MatchesFirst { - // v1 matches the smaller version, so cmp(v1, v2) should be < 0 + case v1MatchesFirst: if cmp >= 0 { t.Errorf("CompareWithScheme(%q, %q, %q) = %d, want < 0 (expected order: %v)", v1, v2, input.InputScheme, cmp, expected) } - } else { - // v1 matches the larger version, so cmp(v1, v2) should be > 0 + default: if cmp <= 0 { t.Errorf("CompareWithScheme(%q, %q, %q) = %d, want > 0 (expected order: %v)", v1, v2, input.InputScheme, cmp, expected) } diff --git a/interval.go b/interval.go index a7a1f43..6c2e596 100644 --- a/interval.go +++ b/interval.go @@ -114,43 +114,47 @@ func (i Interval) Intersect(other Interval) Interval { result := Interval{} // Determine new minimum - if i.Min != "" && other.Min != "" { + switch { + case i.Min != "" && other.Min != "": cmp := CompareVersions(i.Min, other.Min) - if cmp > 0 { + switch { + case cmp > 0: result.Min = i.Min result.MinInclusive = i.MinInclusive - } else if cmp < 0 { + case cmp < 0: result.Min = other.Min result.MinInclusive = other.MinInclusive - } else { + default: result.Min = i.Min result.MinInclusive = i.MinInclusive && other.MinInclusive } - } else if i.Min != "" { + case i.Min != "": result.Min = i.Min result.MinInclusive = i.MinInclusive - } else if other.Min != "" { + case other.Min != "": result.Min = other.Min result.MinInclusive = other.MinInclusive } // Determine new maximum - if i.Max != "" && other.Max != "" { + switch { + case i.Max != "" && other.Max != "": cmp := CompareVersions(i.Max, other.Max) - if cmp < 0 { + switch { + case cmp < 0: result.Max = i.Max result.MaxInclusive = i.MaxInclusive - } else if cmp > 0 { + case cmp > 0: result.Max = other.Max result.MaxInclusive = other.MaxInclusive - } else { + default: result.Max = i.Max result.MaxInclusive = i.MaxInclusive && other.MaxInclusive } - } else if i.Max != "" { + case i.Max != "": result.Max = i.Max result.MaxInclusive = i.MaxInclusive - } else if other.Max != "" { + case other.Max != "": result.Max = other.Max result.MaxInclusive = other.MaxInclusive } @@ -200,18 +204,18 @@ func (i Interval) Union(other Interval) *Interval { // Determine new minimum (take the smaller one, "" means unbounded) if i.Min == "" || other.Min == "" { - // Either side is unbounded below, so the union is too result.Min = "" result.MinInclusive = false } else { cmp := CompareVersions(i.Min, other.Min) - if cmp < 0 { + switch { + case cmp < 0: result.Min = i.Min result.MinInclusive = i.MinInclusive - } else if cmp > 0 { + case cmp > 0: result.Min = other.Min result.MinInclusive = other.MinInclusive - } else { + default: result.Min = i.Min result.MinInclusive = i.MinInclusive || other.MinInclusive } @@ -219,18 +223,18 @@ func (i Interval) Union(other Interval) *Interval { // Determine new maximum (take the larger one, "" means unbounded) if i.Max == "" || other.Max == "" { - // Either side is unbounded above, so the union is too result.Max = "" result.MaxInclusive = false } else { cmp := CompareVersions(i.Max, other.Max) - if cmp > 0 { + switch { + case cmp > 0: result.Max = i.Max result.MaxInclusive = i.MaxInclusive - } else if cmp < 0 { + case cmp < 0: result.Max = other.Max result.MaxInclusive = other.MaxInclusive - } else { + default: result.Max = i.Max result.MaxInclusive = i.MaxInclusive || other.MaxInclusive } diff --git a/interval_test.go b/interval_test.go index ebbc0c9..39f9e08 100644 --- a/interval_test.go +++ b/interval_test.go @@ -4,10 +4,10 @@ import "testing" func TestNewInterval(t *testing.T) { i := NewInterval("1.0.0", "2.0.0", true, false) - if i.Min != "1.0.0" { + if i.Min != "1.0.0" { //nolint:goconst t.Errorf("Min = %q, want %q", i.Min, "1.0.0") } - if i.Max != "2.0.0" { + if i.Max != "2.0.0" { //nolint:goconst t.Errorf("Max = %q, want %q", i.Max, "2.0.0") } if !i.MinInclusive { @@ -176,7 +176,7 @@ func TestIntervalIntersect(t *testing.T) { NewInterval("1.0.0", "3.0.0", true, true), NewInterval("2.0.0", "4.0.0", true, true), func(r Interval) bool { - return r.Min == "2.0.0" && r.Max == "3.0.0" && r.MinInclusive && r.MaxInclusive + return r.Min == "2.0.0" && r.Max == "3.0.0" && r.MinInclusive && r.MaxInclusive //nolint:goconst }, }, { diff --git a/parser.go b/parser.go index 1125b1d..ffd4664 100644 --- a/parser.go +++ b/parser.go @@ -45,7 +45,7 @@ func (p *Parser) ParseNative(constraint string, scheme string) (*Range, error) { return p.parsePypiRange(constraint) case "maven": return p.parseMavenRange(constraint) - case "nuget": + case "nuget": //nolint:goconst return p.parseNugetRange(constraint) case "cargo": return p.parseCargoRange(constraint) @@ -359,7 +359,7 @@ func (p *Parser) parseNpmSingleRange(s string) (*Range, error) { // Hyphen range: 1.2.3 - 2.0.0 if strings.Contains(s, " - ") { - parts := strings.SplitN(s, " - ", 2) + parts := strings.SplitN(s, " - ", 2) //nolint:mnd return NewRange([]Interval{ NewInterval(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]), true, true), }), nil @@ -405,11 +405,12 @@ func (p *Parser) parseCaretRange(version string) (*Range, error) { } var upper string - if v.Major > 0 { + switch { + case v.Major > 0: upper = fmt.Sprintf("%d.0.0", v.Major+1) - } else if v.Minor > 0 { + case v.Minor > 0: upper = fmt.Sprintf("0.%d.0", v.Minor+1) - } else { + default: upper = fmt.Sprintf("0.0.%d", v.Patch+1) } @@ -446,7 +447,7 @@ func (p *Parser) parseTildeRange(version string) (*Range, error) { segments := strings.Count(version, ".") + 1 var upper string - if segments >= 2 { + if segments >= 2 { //nolint:mnd // ~1.2.3 := >=1.2.3 <1.3.0 // ~1.0.0 := >=1.0.0 <1.1.0 // ~1.0 := >=1.0.0 <1.1.0 @@ -532,14 +533,11 @@ func (p *Parser) parsePessimisticRange(version string) (*Range, error) { segments := strings.Count(version, ".") + 1 var upper string - if segments >= 3 { + if segments >= 3 { //nolint:mnd // ~> 1.2.3 bumps minor: < 1.3 upper = fmt.Sprintf("%d.%d", v.Major, v.Minor+1) - } else if segments == 2 { - // ~> 1.2 bumps major: < 2.0 - upper = fmt.Sprintf("%d.0", v.Major+1) } else { - // ~> 1 bumps major: < 2.0 + // ~> 1.2 or ~> 1 bumps major: < 2.0 upper = fmt.Sprintf("%d.0", v.Major+1) } @@ -593,7 +591,7 @@ func (p *Parser) parseBracketRange(s string) (*Range, error) { maxInclusive := s[len(s)-1] == ']' inner := s[1 : len(s)-1] - parts := strings.SplitN(inner, ",", 2) + parts := strings.SplitN(inner, ",", 2) //nolint:mnd if len(parts) == 1 { // Exact version: [1.0] diff --git a/version.go b/version.go index 68d8e22..f5e71c5 100644 --- a/version.go +++ b/version.go @@ -17,7 +17,7 @@ var simpleNumericRegex = regexp.MustCompile(`^\d+$`) // versionCache caches parsed versions to avoid re-parsing the same strings. var versionCache = &boundedCache{ items: make(map[string]*VersionInfo), - max: 10000, + max: 10000, //nolint:mnd } type boundedCache struct { @@ -58,79 +58,86 @@ func ParseVersion(s string) (*VersionInfo, error) { return nil, fmt.Errorf("empty version string") } - // Check cache first if cached, ok := versionCache.Load(s); ok { return cached, nil } + v, err := parseVersionUncached(s) + if err != nil { + return nil, err + } + versionCache.Store(s, v) + return v, nil +} + +func parseVersionUncached(s string) (*VersionInfo, error) { v := &VersionInfo{Original: s} - // Handle simple numeric versions if simpleNumericRegex.MatchString(s) { - major, _ := strconv.Atoi(s) - v.Major = major - versionCache.Store(s, v) + v.Major, _ = strconv.Atoi(s) return v, nil } - // Try semantic version parsing if matches := SemanticVersionRegex.FindStringSubmatch(s); matches != nil { - if matches[1] != "" { - v.Major, _ = strconv.Atoi(matches[1]) - } - if matches[2] != "" { - v.Minor, _ = strconv.Atoi(matches[2]) - } - if matches[3] != "" { - v.Patch, _ = strconv.Atoi(matches[3]) - } - v.Prerelease = matches[4] - v.Build = matches[5] - versionCache.Store(s, v) - return v, nil + return parseSemverMatches(v, matches), nil } - // Handle dot-separated versions if strings.Contains(s, ".") { - parts := strings.Split(s, ".") - if len(parts) >= 1 { - v.Major, _ = strconv.Atoi(parts[0]) - } - if len(parts) >= 2 && !strings.Contains(parts[1], "-") { - v.Minor, _ = strconv.Atoi(parts[1]) - } - if len(parts) >= 3 { - if strings.Contains(parts[2], "-") { - patchParts := strings.SplitN(parts[2], "-", 2) - v.Patch, _ = strconv.Atoi(patchParts[0]) - if len(patchParts) > 1 { - v.Prerelease = patchParts[1] - } - } else { - v.Patch, _ = strconv.Atoi(parts[2]) - } - } - if len(parts) > 3 && v.Prerelease == "" { - v.Prerelease = strings.Join(parts[3:], ".") - } - versionCache.Store(s, v) - return v, nil + return parseDotSeparated(v, s), nil } - // Handle dash-separated versions if strings.Contains(s, "-") { - parts := strings.SplitN(s, "-", 2) + parts := strings.SplitN(s, "-", 2) //nolint:mnd v.Major, _ = strconv.Atoi(parts[0]) if len(parts) > 1 { v.Prerelease = parts[1] } - versionCache.Store(s, v) return v, nil } return nil, fmt.Errorf("invalid version format: %s", s) } +func parseSemverMatches(v *VersionInfo, matches []string) *VersionInfo { + if matches[1] != "" { + v.Major, _ = strconv.Atoi(matches[1]) + } + if matches[2] != "" { + v.Minor, _ = strconv.Atoi(matches[2]) + } + if matches[3] != "" { + v.Patch, _ = strconv.Atoi(matches[3]) + } + v.Prerelease = matches[4] + v.Build = matches[5] + return v +} + +func parseDotSeparated(v *VersionInfo, s string) *VersionInfo { + parts := strings.Split(s, ".") + if len(parts) >= 1 { + v.Major, _ = strconv.Atoi(parts[0]) + } + if len(parts) >= 2 && !strings.Contains(parts[1], "-") { //nolint:mnd + v.Minor, _ = strconv.Atoi(parts[1]) + } + if len(parts) >= 3 { //nolint:mnd + if strings.Contains(parts[2], "-") { + patchParts := strings.SplitN(parts[2], "-", 2) //nolint:mnd + v.Patch, _ = strconv.Atoi(patchParts[0]) + if len(patchParts) > 1 { + v.Prerelease = patchParts[1] + } + } else { + v.Patch, _ = strconv.Atoi(parts[2]) + } + } + if len(parts) > 3 && v.Prerelease == "" { //nolint:mnd + v.Prerelease = strings.Join(parts[3:], ".") + } + return v +} + // String returns the normalized version string. func (v *VersionInfo) String() string { result := fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch) @@ -266,7 +273,7 @@ func CompareWithScheme(a, b, scheme string) int { } switch scheme { - case "nuget": + case "nuget": //nolint:goconst return compareNuGet(a, b) case "maven": return compareMaven(a, b) @@ -424,140 +431,101 @@ func compareMaven(a, b string) int { } // compareMavenComponentsNew compares two Maven components with proper handling of sublist vs direct items. -// Key Maven rules: -// - Sublist (afterDash) vs direct numeric: sublist < numeric (list < int) -// - Sublist vs direct string: sublist > string (list > string) -// - Sublist vs null: depends on sublist's first element compared to null -// - Direct vs null: positive numeric > null, zero = null, qualifier uses ordering func compareMavenComponentsNew(a, b mavenComponent) int { - // Both null if a.isNull && b.isNull { return 0 } - - // One is null if a.isNull { - return compareMavenToNull(b, true) * -1 // flip result since we're comparing null to b + return -compareMavenToNull(b) } if b.isNull { - return compareMavenToNull(a, false) + return compareMavenToNull(a) } - // Check if they're at different "levels" (one is sublist, one is direct) if a.afterDash != b.afterDash { - // Different levels - if a.afterDash { - // a is sublist item, b is direct item - if b.isNumeric { - return -1 // sublist < direct numeric (list < int) - } - return 1 // sublist > direct string (list > string) - } - // b is sublist item, a is direct item - if a.isNumeric { - return 1 // direct numeric > sublist (int > list) - } - return -1 // direct string < sublist (string < list) + return compareMavenDifferentLevels(a, b) } - // Same level - compare values normally - // Both numeric - if a.isNumeric && b.isNumeric { - if a.numeric < b.numeric { - return -1 - } - if a.numeric > b.numeric { - return 1 + return compareMavenSameLevel(a, b) +} + +func compareMavenDifferentLevels(a, b mavenComponent) int { + if a.afterDash { + if b.isNumeric { + return -1 // sublist < direct numeric } - return 0 + return 1 // sublist > direct string } + if a.isNumeric { + return 1 // direct numeric > sublist + } + return -1 // direct string < sublist +} - // Numeric vs qualifier (at same level) - if a.isNumeric && !b.isNumeric { +func compareMavenSameLevel(a, b mavenComponent) int { + if a.isNumeric && b.isNumeric { + return cmpInt(a.numeric, b.numeric) + } + if a.isNumeric { return 1 // numeric > any qualifier } - if !a.isNumeric && b.isNumeric { + if b.isNumeric { return -1 // qualifier < numeric } - // Both qualifiers - use qualifier ordering orderA, okA := getMavenQualifierOrder(a.qualifier) orderB, okB := getMavenQualifierOrder(b.qualifier) - if orderA != orderB { - if orderA < orderB { - return -1 - } + return cmpInt(orderA, orderB) + } + if !okA && !okB { + return cmpString(a.qualifier, b.qualifier) + } + return 0 +} + +func cmpInt(a, b int) int { + if a < b { + return -1 + } + if a > b { return 1 } + return 0 +} - // Same order - if both unknown, compare alphabetically - if !okA && !okB { - if a.qualifier < b.qualifier { - return -1 - } - if a.qualifier > b.qualifier { - return 1 - } +func cmpString(a, b string) int { + if a < b { + return -1 + } + if a > b { + return 1 } return 0 } // compareMavenToNull compares a component to null (missing component). -// Returns the comparison result (component vs null). -// In Maven: -// - Numeric: positive > null, zero = null, negative < null -// - Qualifier: uses qualifier ordering vs release ("") -// - Sublist: depends on first element of the sublist -func compareMavenToNull(comp mavenComponent, flip bool) int { - var result int - - if comp.afterDash { - // Sublist vs null: compare first element to null - // A sublist [x] vs null → x.compareTo(null) - if comp.isNumeric { - if comp.numeric == 0 { - result = 0 - } else if comp.numeric > 0 { - result = 1 // positive > null - } else { - result = -1 - } - } else { - // Qualifier in sublist vs null - orderComp, _ := getMavenQualifierOrder(comp.qualifier) - orderNull := mavenQualifierOrder[""] - if orderComp < orderNull { - result = -1 // prerelease qualifiers < null - } else if orderComp > orderNull { - result = 1 // sp, unknown > null - } else { - result = 0 - } - } - } else { - // Direct item vs null - if comp.isNumeric { - if comp.numeric == 0 { - result = 0 // zero = null - } else { - result = 1 // positive > null - } - } else { - // Direct qualifier vs null (release "") - orderComp, _ := getMavenQualifierOrder(comp.qualifier) - orderNull := mavenQualifierOrder[""] - if orderComp < orderNull { - result = -1 - } else if orderComp > orderNull { - result = 1 - } else { - result = 0 - } - } +func compareMavenToNull(comp mavenComponent) int { + if comp.isNumeric { + return compareMavenNumericToNull(comp) } + return compareMavenQualifierToNull(comp.qualifier) +} - return result +func compareMavenNumericToNull(comp mavenComponent) int { + if comp.numeric == 0 { + return 0 + } + if comp.afterDash && comp.numeric < 0 { + return -1 + } + return 1 +} + +func compareMavenQualifierToNull(qualifier string) int { + orderComp, _ := getMavenQualifierOrder(qualifier) + orderNull := mavenQualifierOrder[""] + return cmpInt(orderComp, orderNull) } type mavenComponent struct { @@ -570,6 +538,8 @@ type mavenComponent struct { // Maven qualifier ordering // Order: alpha < beta < milestone < rc < snapshot < "" (release) < sp < unknown < numbers +// +//nolint:mnd var mavenQualifierOrder = map[string]int{ "alpha": 1, "beta": 2, @@ -585,8 +555,7 @@ func getMavenQualifierOrder(q string) (int, bool) { if ok { return order, true } - // Unknown qualifiers get order 8 (after sp, before numbers which get 9) - return 8, false + return 8, false //nolint:mnd } func parseMavenVersion(s string) []mavenComponent {