Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 25 additions & 13 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,23 +260,30 @@ func (p *Parser) parseNpmRange(s string) (*Range, error) {
return Unbounded(), nil
}

// Handle || (OR)
// Handle || (OR) -- collect all parts, then merge once
if strings.Contains(s, "||") {
parts := strings.Split(s, "||")
var result *Range
var allIntervals []Interval
var allExclusions []string
var allRaw []Interval
for _, part := range parts {
// Each OR part may contain AND constraints, so recurse
r, err := p.parseNpmRange(strings.TrimSpace(part))
if err != nil {
return nil, err
}
if result == nil {
result = r
allIntervals = append(allIntervals, r.Intervals...)
allExclusions = append(allExclusions, r.Exclusions...)
if len(r.RawConstraints) > 0 {
allRaw = append(allRaw, r.RawConstraints...)
} else {
result = result.Union(r)
allRaw = append(allRaw, r.Intervals...)
}
}
return result, nil
return &Range{
Intervals: mergeIntervals(allIntervals),
Exclusions: allExclusions,
RawConstraints: allRaw,
}, nil
}

// Handle space-separated AND constraints
Expand Down Expand Up @@ -661,22 +668,27 @@ func (p *Parser) parseGoRange(s string) (*Range, error) {
func (p *Parser) parseHexRange(s string) (*Range, error) {
s = strings.TrimSpace(s)

// Handle "or" disjunction first
// Handle "or" disjunction -- collect all parts, then merge once
if strings.Contains(s, " or ") {
parts := strings.Split(s, " or ")
var result *Range
var allIntervals []Interval
var allRaw []Interval
for _, part := range parts {
r, err := p.parseHexSingleRange(strings.TrimSpace(part))
if err != nil {
return nil, err
}
if result == nil {
result = r
allIntervals = append(allIntervals, r.Intervals...)
if len(r.RawConstraints) > 0 {
allRaw = append(allRaw, r.RawConstraints...)
} else {
result = result.Union(r)
allRaw = append(allRaw, r.Intervals...)
}
}
return result, nil
return &Range{
Intervals: mergeIntervals(allIntervals),
RawConstraints: allRaw,
}, nil
}

return p.parseHexSingleRange(s)
Expand Down
25 changes: 24 additions & 1 deletion parser_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package vers

import "testing"
import (
"fmt"
"strings"
"testing"
)

func TestParseVersURI(t *testing.T) {
tests := []struct {
Expand Down Expand Up @@ -530,3 +534,22 @@ func TestPublicAPISatisfies(t *testing.T) {
})
}
}

func TestNpmParseManyClauses(t *testing.T) {
// Build a constraint with many || parts. Before the fix, this was O(n^3)
// and would take seconds or minutes. Now it should complete quickly.
parts := make([]string, 500)
for i := range parts {
parts[i] = fmt.Sprintf(">=%d.0.0 <%d.0.0", i, i+1)
}
input := strings.Join(parts, " || ")

p := NewParser()
r, err := p.ParseNative(input, "npm")
if err != nil {
t.Fatalf("Parse failed: %v", err)
}
if r == nil || r.IsEmpty() {
t.Error("expected non-empty range")
}
}
50 changes: 34 additions & 16 deletions range.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package vers

import "strings"
import (
"sort"
"strings"
)

// Range represents a version range as a collection of intervals.
// Multiple intervals represent a union (OR) of ranges.
Expand Down Expand Up @@ -198,24 +201,39 @@ func mergeIntervals(intervals []Interval) []Interval {
return intervals
}

// Simple implementation: try to merge each pair
result := make([]Interval, 0, len(intervals))

for _, interval := range intervals {
if interval.IsEmpty() {
continue
// Filter empty intervals and sort by lower bound
sorted := make([]Interval, 0, len(intervals))
for _, iv := range intervals {
if !iv.IsEmpty() {
sorted = append(sorted, iv)
}
}
if len(sorted) == 0 {
return nil
}

merged := false
for i, existing := range result {
if union := existing.Union(interval); union != nil {
result[i] = *union
merged = true
break
}
sort.Slice(sorted, func(i, j int) bool {
a, b := sorted[i], sorted[j]
if a.Min == "" && b.Min != "" {
return true // unbounded lower comes first
}
if a.Min != "" && b.Min == "" {
return false
}
cmp := CompareVersions(a.Min, b.Min)
if cmp != 0 {
return cmp < 0
}
if !merged {
result = append(result, interval)
return a.MinInclusive && !b.MinInclusive
})

result := []Interval{sorted[0]}
for _, iv := range sorted[1:] {
last := &result[len(result)-1]
if union := last.Union(iv); union != nil {
*last = *union
} else {
result = append(result, iv)
}
}

Expand Down