blob: bf2f500380b873f7bd61519ad2fa163e1f3c5628 [file] [log] [blame]
package semver
import (
"fmt"
"regexp"
"sort"
"strings"
"sync"
)
var constraintRegex *regexp.Regexp
var constraintRangeRegex *regexp.Regexp
const cvRegex string = `v?([0-9|x|X|\*]+)(\.[0-9|x|X|\*]+)?(\.[0-9|x|X|\*]+)?` +
`(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` +
`(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?`
func init() {
constraintOps := []string{
"",
"=",
"!=",
">",
"<",
">=",
"=>",
"<=",
"=<",
"~",
"~>",
"^",
}
ops := make([]string, 0, len(constraintOps))
for _, op := range constraintOps {
ops = append(ops, regexp.QuoteMeta(op))
}
constraintRegex = regexp.MustCompile(fmt.Sprintf(
`^\s*(%s)\s*(%s)\s*$`,
strings.Join(ops, "|"),
cvRegex))
constraintRangeRegex = regexp.MustCompile(fmt.Sprintf(
`\s*(%s)\s*-\s*(%s)\s*`,
cvRegex, cvRegex))
}
type Constraint interface {
// Constraints compose the fmt.Stringer interface. Printing a constraint
// will yield a string that, if passed to NewConstraint(), will produce the
// original constraint. (Bidirectional serialization)
fmt.Stringer
// Matches checks that a version satisfies the constraint. If it does not,
// an error is returned indcating the problem; if it does, the error is nil.
Matches(v *Version) error
// Intersect computes the intersection between the receiving Constraint and
// passed Constraint, and returns a new Constraint representing the result.
Intersect(Constraint) Constraint
// Union computes the union between the receiving Constraint and the passed
// Constraint, and returns a new Constraint representing the result.
Union(Constraint) Constraint
// MatchesAny returns a bool indicating whether there exists any version that
// satisfies both the receiver constraint, and the passed Constraint.
//
// In other words, this reports whether an intersection would be non-empty.
MatchesAny(Constraint) bool
// Restrict implementation of this interface to this package. We need the
// flexibility of an interface, but we cover all possibilities here; closing
// off the interface to external implementation lets us safely do tricks
// with types for magic types (none and any)
_private()
}
// realConstraint is used internally to differentiate between any, none, and
// unionConstraints, vs. Version and rangeConstraints.
type realConstraint interface {
Constraint
_real()
}
// Controls whether or not parsed constraints are cached
var CacheConstraints = true
var constraintCache = make(map[string]ccache)
var constraintCacheLock sync.RWMutex
type ccache struct {
c Constraint
err error
}
// NewConstraint takes a string representing a set of semver constraints, and
// returns a corresponding Constraint object. Constraints are suitable
// for checking Versions for admissibility, or combining with other Constraint
// objects.
//
// If an invalid constraint string is passed, more information is provided in
// the returned error string.
func NewConstraint(in string) (Constraint, error) {
if CacheConstraints {
constraintCacheLock.RLock()
if final, exists := constraintCache[in]; exists {
constraintCacheLock.RUnlock()
return final.c, final.err
}
constraintCacheLock.RUnlock()
}
// Rewrite - ranges into a comparison operation.
c := rewriteRange(in)
ors := strings.Split(c, "||")
or := make([]Constraint, len(ors))
for k, v := range ors {
cs := strings.Split(v, ",")
result := make([]Constraint, len(cs))
for i, s := range cs {
pc, err := parseConstraint(s)
if err != nil {
if CacheConstraints {
constraintCacheLock.Lock()
constraintCache[in] = ccache{err: err}
constraintCacheLock.Unlock()
}
return nil, err
}
result[i] = pc
}
or[k] = Intersection(result...)
}
final := Union(or...)
if CacheConstraints {
constraintCacheLock.Lock()
constraintCache[in] = ccache{c: final}
constraintCacheLock.Unlock()
}
return final, nil
}
// Intersection computes the intersection between N Constraints, returning as
// compact a representation of the intersection as possible.
//
// No error is indicated if all the sets are collectively disjoint; you must inspect the
// return value to see if the result is the empty set (by calling IsNone() on
// it).
func Intersection(cg ...Constraint) Constraint {
// If there's zero or one constraints in the group, we can quit fast
switch len(cg) {
case 0:
// Zero members, only sane thing to do is return none
return None()
case 1:
// Just one member means that's our final constraint
return cg[0]
}
car, cdr := cg[0], cg[1:]
for _, c := range cdr {
if IsNone(car) {
return None()
}
car = car.Intersect(c)
}
return car
}
// Union takes a variable number of constraints, and returns the most compact
// possible representation of those constraints.
//
// This effectively ORs together all the provided constraints. If any of the
// included constraints are the set of all versions (any), that supercedes
// everything else.
func Union(cg ...Constraint) Constraint {
// If there's zero or one constraints in the group, we can quit fast
switch len(cg) {
case 0:
// Zero members, only sane thing to do is return none
return None()
case 1:
// One member, so the result will just be that
return cg[0]
}
// Preliminary pass to look for 'any' in the current set (and bail out early
// if found), but also construct a []realConstraint for everything else
var real constraintList
for _, c := range cg {
switch tc := c.(type) {
case any:
return c
case none:
continue
case *Version:
//if tc != nil {
//heap.Push(&real, tc)
//}
real = append(real, tc)
case rangeConstraint:
//heap.Push(&real, tc)
real = append(real, tc)
case unionConstraint:
real = append(real, tc...)
//for _, c2 := range tc {
//heap.Push(&real, c2)
//}
default:
panic("unknown constraint type")
}
}
// TODO wtf why isn't heap working...so, ugh, have to do this
// Sort both the versions and ranges into ascending order
sort.Sort(real)
// Iteratively merge the constraintList elements
var nuc unionConstraint
for _, c := range real {
if len(nuc) == 0 {
nuc = append(nuc, c)
continue
}
last := nuc[len(nuc)-1]
switch lt := last.(type) {
case *Version:
switch ct := c.(type) {
case *Version:
// Two versions in a row; only append if they're not equal
if !lt.Equal(ct) {
nuc = append(nuc, ct)
}
case rangeConstraint:
// Last was version, current is range. constraintList sorts by
// min version, so it's guaranteed that the version will be less
// than the range's min, guaranteeing that these are disjoint.
//
// ...almost. If the min of the range is the same as the
// version, then a union should merge the two by making the
// range inclusive at the bottom.
if lt.Equal(ct.min) {
ct.includeMin = true
nuc[len(nuc)-1] = ct
} else {
nuc = append(nuc, c)
}
}
case rangeConstraint:
switch ct := c.(type) {
case *Version:
// Last was range, current is version. constraintList sort invariants guarantee
// that the version will be greater than the min, so we have to
// determine if the version is less than the max. If it is, we
// subsume it into the range with a Union call.
//
// Lazy version: just union them and let rangeConstraint figure
// it out, then switch on the result type.
c2 := lt.Union(ct)
if crc, ok := c2.(realConstraint); ok {
nuc[len(nuc)-1] = crc
} else {
// Otherwise, all it can be is a union constraint. First
// item in the union will be the same range, second will be the
// version, so append onto nuc from one back from the end
nuc = append(nuc[:len(nuc)-1], c2.(unionConstraint)...)
}
case rangeConstraint:
if lt.MatchesAny(ct) || areAdjacent(lt, ct) {
// If the previous range overlaps or is adjacent to the
// current range, we know they'll be able to merge together,
// so overwrite the last item in nuc with the result of that
// merge (which is what Union will produce)
nuc[len(nuc)-1] = lt.Union(ct).(realConstraint)
} else {
nuc = append(nuc, c)
}
}
}
}
if len(nuc) == 1 {
return nuc[0]
}
return nuc
}