Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Intersects function to constraints #169

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
208 changes: 208 additions & 0 deletions constraints.go
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -164,6 +219,12 @@ func init() {
"^": constraintCaret,
}

constraintExpandOps = map[string]cExpandFunc{
"~": constraintExpandTilde,
"~>": constraintExpandTilde,
"^": constraintExpandCaret,
}

ops := `=||!=|>|<|>=|=>|<=|=<|~|~>|\^`

constraintRegex = regexp.MustCompile(fmt.Sprintf(
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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":
Expand Down
85 changes: 85 additions & 0 deletions constraints_test.go
Expand Up @@ -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)
}
}
}