diff --git a/constraints.go b/constraints.go index 547613f..066691b 100644 --- a/constraints.go +++ b/constraints.go @@ -113,6 +113,59 @@ func (cs Constraints) Validate(v *Version) (bool, []error) { return false, e } +// Intersects checks if the both Constraints have an intersection +func (cs Constraints) Intersects(cs2 *Constraints) (bool, error) { + for _, c1s := range cs.constraints { + expandedCs1 := make([]*constraint, len(c1s)) + copy(expandedCs1, c1s) + + for i, c := range c1s { + if expander, ok := constraintExpandOps[c.origfunc]; ok { + expandedCs1 = append(expandedCs1[:i], expandedCs1[i+1:]...) + expandedCs1 = append(expandedCs1, expander(c)...) + } + } + + for _, c2s := range cs2.constraints { + expandedCs2 := make([]*constraint, len(c2s)) + copy(expandedCs2, c2s) + + for i, c := range c2s { + if expander, ok := constraintExpandOps[c.origfunc]; ok { + expandedCs2 = append(expandedCs2[:i], expandedCs2[i+1:]...) + expandedCs2 = append(expandedCs2, expander(c)...) + } + } + + success := true + + for _, c1 := range expandedCs1 { + for _, c2 := range expandedCs2 { + intersects, err := c1.intersects(c2) + + if err != nil { + return false, err + } + + if !intersects { + success = false + break + } + } + + if !success { + break + } + } + + if success { + return true, nil + } + } + } + return false, nil +} + func (cs Constraints) String() string { buf := make([]string, len(cs.constraints)) var tmp bytes.Buffer @@ -138,6 +191,8 @@ var constraintOps map[string]cfunc var constraintRegex *regexp.Regexp var constraintRangeRegex *regexp.Regexp +var constraintExpandOps map[string]cExpandFunc + // Used to find individual constraints within a multi-constraint string var findConstraintRegex *regexp.Regexp @@ -164,6 +219,12 @@ func init() { "^": constraintCaret, } + constraintExpandOps = map[string]cExpandFunc{ + "~": constraintExpandTilde, + "~>": constraintExpandTilde, + "^": constraintExpandCaret, + } + ops := `=||!=|>|<|>=|=>|<=|=<|~|~>|\^` constraintRegex = regexp.MustCompile(fmt.Sprintf( @@ -214,7 +275,50 @@ func (c *constraint) string() string { return c.origfunc + c.orig } +// Intersects checks if both constraints intersect +func (c *constraint) intersects(c2 *constraint) (bool, error) { + if c.string() == c2.string() { + return true, nil + } + + if c.origfunc == "" || c.origfunc == "=" { + return c2.check(c.con) + } else if c2.origfunc == "" || c2.origfunc == "=" { + return c.check(c2.con) + } + + if c.origfunc == "!=" && c2.origfunc == "!=" { + return true, nil + } + + sameDirectionIncreasing := (c.origfunc == ">=" || c.origfunc == "=>" || c.origfunc == ">") && + (c2.origfunc == ">=" || c2.origfunc == "=>" || c2.origfunc == ">") + + sameDirectionDecreasing := (c.origfunc == "<=" || c.origfunc == "=<" || c.origfunc == "<") && + (c2.origfunc == "<=" || c2.origfunc == "=<" || c2.origfunc == "<") + + sameSemVer := c.con.Equal(c2.con) + + differentDirectionsInclusive := (c.origfunc == ">=" || c.origfunc == "=>" || c.origfunc == "<=" || c.origfunc == "=<") && + (c2.origfunc == ">=" || c2.origfunc == "=>" || c2.origfunc == "<=" || c2.origfunc == "=<") + + oppositeDirectionsLessThan := c.con.LessThan(c2.con) && + (c.origfunc == ">=" || c.origfunc == "=>" || c.origfunc == ">") && + (c2.origfunc == "<=" || c2.origfunc == "=<" || c2.origfunc == "<") + + oppositeDirectionsGreaterThan := c.con.GreaterThan(c2.con) && + (c.origfunc == "<=" || c.origfunc == "=<" || c.origfunc == "<") && + (c2.origfunc == ">=" || c2.origfunc == "=>" || c2.origfunc == ">") + + return sameDirectionIncreasing || + sameDirectionDecreasing || + (sameSemVer && differentDirectionsInclusive) || + oppositeDirectionsLessThan || + oppositeDirectionsGreaterThan, nil +} + type cfunc func(v *Version, c *constraint) (bool, error) +type cExpandFunc func(c *constraint) []*constraint func parseConstraint(c string) (*constraint, error) { if len(c) > 0 { @@ -463,6 +567,52 @@ func constraintTilde(v *Version, c *constraint) (bool, error) { return true, nil } +func constraintExpandTilde(c *constraint) []*constraint { + if c.dirty { + return []*constraint{ + { + con: MustParse("0.0.0"), + orig: "0.0.0", + origfunc: ">=", + minorDirty: true, + dirty: true, + patchDirty: true, + }, + } + } + + base := &constraint{ + con: c.con, + orig: c.orig, + origfunc: ">=", + minorDirty: c.minorDirty, + dirty: c.dirty, + patchDirty: c.patchDirty, + } + + if c.minorDirty { + nextMajor := c.con.IncMajor() + return []*constraint{ + base, + { + con: &nextMajor, + orig: nextMajor.String(), + origfunc: "<", + }, + } + } + + nextMinor := c.con.IncMinor() + return []*constraint{ + base, + { + con: &nextMinor, + orig: nextMinor.String(), + origfunc: "<", + }, + } +} + // When there is a .x (dirty) status it automatically opts in to ~. Otherwise // it's a straight = func constraintTildeOrEqual(v *Version, c *constraint) (bool, error) { @@ -544,6 +694,64 @@ func constraintCaret(v *Version, c *constraint) (bool, error) { return false, fmt.Errorf("%s does not equal %s. Expect version and constraint to equal when major and minor versions are 0", v, c.orig) } +func constraintExpandCaret(c *constraint) []*constraint { + if c.dirty { + return []*constraint{ + { + con: MustParse("0.0.0"), + orig: "0.0.0", + origfunc: ">=", + minorDirty: true, + dirty: true, + patchDirty: true, + }, + } + } + + base := &constraint{ + con: c.con, + orig: c.orig, + origfunc: ">=", + minorDirty: c.minorDirty, + dirty: c.dirty, + patchDirty: c.patchDirty, + } + + if c.con.Major() == 0 || c.minorDirty { + if c.con.Minor() == 0 || c.patchDirty { + nextPatch := c.con.IncPatch() + return []*constraint{ + base, + { + con: &nextPatch, + orig: nextPatch.String(), + origfunc: "<", + }, + } + } + + nextMinor := c.con.IncMinor() + return []*constraint{ + base, + { + con: &nextMinor, + orig: nextMinor.String(), + origfunc: "<", + }, + } + } + + nextMajor := c.con.IncMajor() + return []*constraint{ + base, + { + con: &nextMajor, + orig: nextMajor.String(), + origfunc: "<", + }, + } +} + func isX(x string) bool { switch x { case "x", "*", "X": diff --git a/constraints_test.go b/constraints_test.go index 0504399..eb44943 100644 --- a/constraints_test.go +++ b/constraints_test.go @@ -664,3 +664,88 @@ func TestConstraintString(t *testing.T) { } } } + +func TestConstraintIntersect(t *testing.T) { + tests := []struct { + constraint1 string + constraint2 string + intersects bool + }{ + // One is a Version + {"1.3.0", ">=1.3.0", true}, + {"1.3.0", ">1.3.0", false}, + {">=1.3.0", "1.3.0", true}, + {">1.3.0", "1.3.0", false}, + // Same direction increasing + {">1.3.0", ">1.2.0", true}, + {">1.2.0", ">1.3.0", true}, + {">=1.2.0", ">1.3.0", true}, + {">1.2.0", ">=1.3.0", true}, + // Same direction decreasing + {"<1.3.0", "<1.2.0", true}, + {"<1.2.0", "<1.3.0", true}, + {"<=1.2.0", "<1.3.0", true}, + {"<1.2.0", "<=1.3.0", true}, + // Different directions, same semver and inclusive operator + {">=1.3.0", "<=1.3.0", true}, + {">=v1.3.0", "<=1.3.0", true}, + {">=1.3.0", ">=1.3.0", true}, + {"<=1.3.0", "<=1.3.0", true}, + {"<=1.3.0", "<=v1.3.0", true}, + {">1.3.0", "<=1.3.0", false}, + {">=1.3.0", "<1.3.0", false}, + // Opposite matching directions + {">1.0.0", "<2.0.0", true}, + {">=1.0.0", "<2.0.0", true}, + {">=1.0.0", "<=2.0.0", true}, + {">1.0.0", "<=2.0.0", true}, + {"<=2.0.0", ">1.0.0", true}, + {"<=1.0.0", ">=2.0.0", false}, + // Not equals + {"!=1.3.0", "!=1.3.0", true}, + {"1.3.0", "!=1.3.0", false}, + {"!=1.4.0", "!=1.3.0", true}, + // Tilde + {"~1.x.x", "~1.2.x", true}, + {"~1.2.x", "~1.2.3", true}, + {"~1.3.x", "~1.2.3", true}, + {"~1.3.4", "~1.2.3", false}, + {"~2.3.4", "~1.2.3", false}, + {"~0.3.4", "~1.2.3", false}, + {"~0.3.4", "~0.2.3", false}, + {"~0.2.x", "~0.2.3", true}, + // Caret + {"^1.x.x", "^1.2.x", true}, + {"^1.2.x", "^1.2.3", true}, + {"^1.3.x", "^1.2.3", true}, + {"^1.3.4", "^1.2.3", true}, + {"^2.3.4", "^1.2.3", false}, + {"^0.3.4", "^1.2.3", false}, + {"^0.3.4", "^0.2.3", false}, + {"^0.2.x", "^0.2.3", true}, + } + + for _, tc := range tests { + c1, err := NewConstraint(tc.constraint1) + if err != nil { + t.Errorf("err: %s", err) + continue + } + + c2, err := NewConstraint(tc.constraint2) + if err != nil { + t.Errorf("err: %s", err) + continue + } + + a, _ := c1.Intersects(c2) + if a != tc.intersects { + t.Errorf("Constraint %q failing with %q", tc.constraint1, tc.constraint2) + } + + b, _ := c2.Intersects(c1) + if b != tc.intersects { + t.Errorf("Constraint %q failing with %q", tc.constraint2, tc.constraint1) + } + } +}