Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 65 additions & 32 deletions constraints.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,39 @@ func (c *constraint) string() string {

type cfunc func(v *Version, c *constraint, includePre bool) (bool, error)

// constraintErr is a lazily-formatted error returned when a constraint check
// fails. The message (and the Version.String() conversion it requires) is only
// computed when Error() is called. A Check that discards the error therefore
// performs no message formatting and only allocates this small struct.
type constraintErr struct {
format string
v *Version
orig string
hasOrig bool
}

func (e *constraintErr) Error() string {
if e.hasOrig {
return fmt.Sprintf(e.format, e.v, e.orig)
}
return fmt.Sprintf(e.format, e.v)
}

const cerrOneFormat = "%q is a prerelease version and the constraint is only looking for release versions"

// cerrOne builds a constraintErr referencing only the version, deferring all
// message formatting (including the version string conversion) until Error()
// is called.
func cerrOne(v *Version) *constraintErr {
return &constraintErr{format: cerrOneFormat, v: v}
}

// cerrTwo builds a constraintErr referencing the version and the constraint's
// original string, deferring all message formatting until Error() is called.
func cerrTwo(v *Version, c *constraint, format string) *constraintErr {
return &constraintErr{format: format, v: v, orig: c.orig, hasOrig: true}
}

func parseConstraint(c string) (*constraint, error) {
if len(c) > 0 {
m := constraintRegex.FindStringSubmatch(c)
Expand Down Expand Up @@ -347,7 +380,7 @@ func constraintNotEqual(v *Version, c *constraint, includePre bool) (bool, error
// The existence of prereleases is checked at the group level and passed in.
// Exit early if the version has a prerelease but those are to be ignored.
if v.Prerelease() != "" && !includePre {
return false, fmt.Errorf("%q is a prerelease version and the constraint is only looking for release versions", v)
return false, cerrOne(v)
}

if c.dirty {
Expand All @@ -357,7 +390,7 @@ func constraintNotEqual(v *Version, c *constraint, includePre bool) (bool, error
if c.con.Minor() != v.Minor() && !c.minorDirty {
return true, nil
} else if c.minorDirty {
return false, fmt.Errorf("%q is equal to %q", v, c.orig)
return false, cerrTwo(v, c, "%q is equal to %q")
} else if c.con.Patch() != v.Patch() && !c.patchDirty {
return true, nil
} else if c.patchDirty {
Expand All @@ -367,15 +400,15 @@ func constraintNotEqual(v *Version, c *constraint, includePre bool) (bool, error
if eq {
return true, nil
}
return false, fmt.Errorf("%q is equal to %q", v, c.orig)
return false, cerrTwo(v, c, "%q is equal to %q")
}
return false, fmt.Errorf("%q is equal to %q", v, c.orig)
return false, cerrTwo(v, c, "%q is equal to %q")
}
}

eq := v.Equal(c.con)
if eq {
return false, fmt.Errorf("%q is equal to %q", v, c.orig)
return false, cerrTwo(v, c, "%q is equal to %q")
}

return true, nil
Expand All @@ -386,7 +419,7 @@ func constraintGreaterThan(v *Version, c *constraint, includePre bool) (bool, er
// The existence of prereleases is checked at the group level and passed in.
// Exit early if the version has a prerelease but those are to be ignored.
if v.Prerelease() != "" && !includePre {
return false, fmt.Errorf("%q is a prerelease version and the constraint is only looking for release versions", v)
return false, cerrOne(v)
}

var eq bool
Expand All @@ -396,25 +429,25 @@ func constraintGreaterThan(v *Version, c *constraint, includePre bool) (bool, er
if eq {
return true, nil
}
return false, fmt.Errorf("%q is less than or equal to %q", v, c.orig)
return false, cerrTwo(v, c, "%q is less than or equal to %q")
}

if v.Major() > c.con.Major() {
return true, nil
} else if v.Major() < c.con.Major() {
return false, fmt.Errorf("%q is less than or equal to %q", v, c.orig)
return false, cerrTwo(v, c, "%q is less than or equal to %q")
} else if c.minorDirty {
// This is a range case such as >11. When the version is something like
// 11.1.0 is it not > 11. For that we would need 12 or higher
return false, fmt.Errorf("%q is less than or equal to %q", v, c.orig)
return false, cerrTwo(v, c, "%q is less than or equal to %q")
} else if c.patchDirty {
// This is for ranges such as >11.1. A version of 11.1.1 is not greater
// which one of 11.2.1 is greater
eq = v.Minor() > c.con.Minor()
if eq {
return true, nil
}
return false, fmt.Errorf("%q is less than or equal to %q", v, c.orig)
return false, cerrTwo(v, c, "%q is less than or equal to %q")
}

// If we have gotten here we are not comparing pre-preleases and can use the
Expand All @@ -423,43 +456,43 @@ func constraintGreaterThan(v *Version, c *constraint, includePre bool) (bool, er
if eq {
return true, nil
}
return false, fmt.Errorf("%q is less than or equal to %q", v, c.orig)
return false, cerrTwo(v, c, "%q is less than or equal to %q")
}

func constraintLessThan(v *Version, c *constraint, includePre bool) (bool, error) {
// The existence of prereleases is checked at the group level and passed in.
// Exit early if the version has a prerelease but those are to be ignored.
if v.Prerelease() != "" && !includePre {
return false, fmt.Errorf("%q is a prerelease version and the constraint is only looking for release versions", v)
return false, cerrOne(v)
}

eq := v.Compare(c.con) < 0
if eq {
return true, nil
}
return false, fmt.Errorf("%q is greater than or equal to %q", v, c.orig)
return false, cerrTwo(v, c, "%q is greater than or equal to %q")
}

func constraintGreaterThanEqual(v *Version, c *constraint, includePre bool) (bool, error) {

// The existence of prereleases is checked at the group level and passed in.
// Exit early if the version has a prerelease but those are to be ignored.
if v.Prerelease() != "" && !includePre {
return false, fmt.Errorf("%q is a prerelease version and the constraint is only looking for release versions", v)
return false, cerrOne(v)
}

eq := v.Compare(c.con) >= 0
if eq {
return true, nil
}
return false, fmt.Errorf("%q is less than %q", v, c.orig)
return false, cerrTwo(v, c, "%q is less than %q")
}

func constraintLessThanEqual(v *Version, c *constraint, includePre bool) (bool, error) {
// The existence of prereleases is checked at the group level and passed in.
// Exit early if the version has a prerelease but those are to be ignored.
if v.Prerelease() != "" && !includePre {
return false, fmt.Errorf("%q is a prerelease version and the constraint is only looking for release versions", v)
return false, cerrOne(v)
}

var eq bool
Expand All @@ -469,13 +502,13 @@ func constraintLessThanEqual(v *Version, c *constraint, includePre bool) (bool,
if eq {
return true, nil
}
return false, fmt.Errorf("%q is greater than %q", v, c.orig)
return false, cerrTwo(v, c, "%q is greater than %q")
}

if v.Major() > c.con.Major() {
return false, fmt.Errorf("%q is greater than %q", v, c.orig)
return false, cerrTwo(v, c, "%q is greater than %q")
} else if v.Major() == c.con.Major() && v.Minor() > c.con.Minor() && !c.minorDirty {
return false, fmt.Errorf("%q is greater than %q", v, c.orig)
return false, cerrTwo(v, c, "%q is greater than %q")
}

return true, nil
Expand All @@ -491,11 +524,11 @@ func constraintTilde(v *Version, c *constraint, includePre bool) (bool, error) {
// The existence of prereleases is checked at the group level and passed in.
// Exit early if the version has a prerelease but those are to be ignored.
if v.Prerelease() != "" && !includePre {
return false, fmt.Errorf("%q is a prerelease version and the constraint is only looking for release versions", v)
return false, cerrOne(v)
}

if v.LessThan(c.con) {
return false, fmt.Errorf("%q is less than %q", v, c.orig)
return false, cerrTwo(v, c, "%q is less than %q")
}

// ~0.0.0 is a special case where all constraints are accepted. It's
Expand All @@ -506,11 +539,11 @@ func constraintTilde(v *Version, c *constraint, includePre bool) (bool, error) {
}

if v.Major() != c.con.Major() {
return false, fmt.Errorf("%q does not have same major version as %q", v, c.orig)
return false, cerrTwo(v, c, "%q does not have same major version as %q")
}

if v.Minor() != c.con.Minor() && !c.minorDirty {
return false, fmt.Errorf("%q does not have same major and minor version as %q", v, c.orig)
return false, cerrTwo(v, c, "%q does not have same major and minor version as %q")
}

return true, nil
Expand All @@ -522,7 +555,7 @@ func constraintTildeOrEqual(v *Version, c *constraint, includePre bool) (bool, e
// The existence of prereleases is checked at the group level and passed in.
// Exit early if the version has a prerelease but those are to be ignored.
if v.Prerelease() != "" && !includePre {
return false, fmt.Errorf("%q is a prerelease version and the constraint is only looking for release versions", v)
return false, cerrOne(v)
}

if c.dirty {
Expand All @@ -534,7 +567,7 @@ func constraintTildeOrEqual(v *Version, c *constraint, includePre bool) (bool, e
return true, nil
}

return false, fmt.Errorf("%q is not equal to %q", v, c.orig)
return false, cerrTwo(v, c, "%q is not equal to %q")
}

// ^* --> (any)
Expand All @@ -550,12 +583,12 @@ func constraintCaret(v *Version, c *constraint, includePre bool) (bool, error) {
// The existence of prereleases is checked at the group level and passed in.
// Exit early if the version has a prerelease but those are to be ignored.
if v.Prerelease() != "" && !includePre {
return false, fmt.Errorf("%q is a prerelease version and the constraint is only looking for release versions", v)
return false, cerrOne(v)
}

// This less than handles prereleases
if v.LessThan(c.con) {
return false, fmt.Errorf("%q is less than %q", v, c.orig)
return false, cerrTwo(v, c, "%q is less than %q")
}

var eq bool
Expand All @@ -570,24 +603,24 @@ func constraintCaret(v *Version, c *constraint, includePre bool) (bool, error) {
if eq {
return true, nil
}
return false, fmt.Errorf("%q does not have same major version as %q", v, c.orig)
return false, cerrTwo(v, c, "%q does not have same major version as %q")
}

// ^ when the major is 0 and minor > 0 is >=0.y.z < 0.y+1
if c.con.Major() == 0 && v.Major() > 0 {
return false, fmt.Errorf("%q does not have same major version as %q", v, c.orig)
return false, cerrTwo(v, c, "%q does not have same major version as %q")
}
// If the con Minor is > 0 it is not dirty
if c.con.Minor() > 0 || c.patchDirty {
eq = v.Minor() == c.con.Minor()
if eq {
return true, nil
}
return false, fmt.Errorf("%q does not have same minor version as %q. Expected minor versions to match when constraint major version is 0", v, c.orig)
return false, cerrTwo(v, c, "%q does not have same minor version as %q. Expected minor versions to match when constraint major version is 0")
}
// ^ when the minor is 0 and minor > 0 is =0.0.z
if c.con.Minor() == 0 && v.Minor() > 0 {
return false, fmt.Errorf("%q does not have same minor version as %q", v, c.orig)
return false, cerrTwo(v, c, "%q does not have same minor version as %q")
}

// At this point the major is 0 and the minor is 0 and not dirty. The patch
Expand All @@ -596,7 +629,7 @@ func constraintCaret(v *Version, c *constraint, includePre bool) (bool, error) {
if eq {
return true, nil
}
return false, fmt.Errorf("%q does not equal %q. Expect version and constraint to equal when major and minor versions are 0", v, c.orig)
return false, cerrTwo(v, c, "%q does not equal %q. Expect version and constraint to equal when major and minor versions are 0")
}

func isX(x string) bool {
Expand Down
36 changes: 36 additions & 0 deletions constraints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,42 @@ func TestConstraintsValidate(t *testing.T) {
}
}

// TestConstraintErrMessageOne pins the lazy one-argument branch of
// constraintErr.Error() (built by cerrOne); the prerelease guard inside each
// constraint func is the only caller of cerrOne and is otherwise only reached
// when the error is discarded (Check) or re-wrapped by Validate, so it needs
// the message read directly here.
func TestConstraintErrMessageOne(t *testing.T) {
c := &constraint{orig: "2.0.0", con: MustParse("2.0.0")}
v := MustParse("3.1.4-beta")

ok, err := constraintGreaterThan(v, c, false)
if ok {
t.Fatal("expected constraint check to fail for a prerelease when prereleases are excluded")
}
got, want := err.Error(), `"3.1.4-beta" is a prerelease version and the constraint is only looking for release versions`
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}

// TestConstraintErrMessageTwo pins the lazy two-argument branch of
// constraintErr.Error() (built by cerrTwo) end-to-end, reading the message
// straight from the returned error rather than through Validate's own copy.
func TestConstraintErrMessageTwo(t *testing.T) {
c := &constraint{orig: "2.0.0", con: MustParse("2.0.0")}
v := MustParse("1.0.0")

ok, err := constraintGreaterThan(v, c, false)
if ok {
t.Fatal("expected constraint check to fail for a lower version")
}
got, want := err.Error(), `"1.0.0" is less than or equal to "2.0.0"`
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}

func TestConstraintsValidateIncludePrerelease(t *testing.T) {
tests := []struct {
constraint string
Expand Down
Loading
Loading