Skip to content

Commit 6d9e2e8

Browse files
committed
More accurate validation messages on constraints
Previously the validation messages were simply tied to the operator. There were cases where the message lost its meaning. For example, a constraing of ^1.12.7 with a version to check of 1.6.6 would have said the major version did not match. This is not true. The errors were reworked to be part of the functions performing the check. This would allow some errors returned to be different from others. For example, in the case listed here it could not the one is less than the other. The varying cases is important for ^ as it has special rules for cases when the major is 0 or the major and minor are 0. Validation messages appropriate for each of those cases can be returned. Note, in v4 of this library there can be one function for checking and validation as the internals enable that now. The Check function on constraints can return errors with it and the Validation method can be removed. That wasn't put into place now as it would be API breaking under semver rules. Closes #145
1 parent 0ce76fe commit 6d9e2e8

2 files changed

Lines changed: 129 additions & 82 deletions

File tree

constraints.go

Lines changed: 109 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,13 @@ func NewConstraint(c string) (*Constraints, error) {
5454

5555
// Check tests if a version satisfies the constraints.
5656
func (cs Constraints) Check(v *Version) bool {
57+
// TODO(mattfarina): For v4 of this library consolidate the Check and Validate
58+
// functions as the underlying functions make that possible now.
5759
// loop over the ORs and check the inner ANDs
5860
for _, o := range cs.constraints {
5961
joy := true
6062
for _, c := range o {
61-
if !c.check(v) {
63+
if check, _ := c.check(v); !check {
6264
joy = false
6365
break
6466
}
@@ -96,9 +98,8 @@ func (cs Constraints) Validate(v *Version) (bool, []error) {
9698

9799
} else {
98100

99-
if !c.check(v) {
100-
em := fmt.Errorf(constraintMsg[c.origfunc], v, c.orig)
101-
e = append(e, em)
101+
if _, err := c.check(v); err != nil {
102+
e = append(e, err)
102103
joy = false
103104
}
104105
}
@@ -134,7 +135,6 @@ func (cs Constraints) String() string {
134135
}
135136

136137
var constraintOps map[string]cfunc
137-
var constraintMsg map[string]string
138138
var constraintRegex *regexp.Regexp
139139
var constraintRangeRegex *regexp.Regexp
140140

@@ -164,21 +164,6 @@ func init() {
164164
"^": constraintCaret,
165165
}
166166

167-
constraintMsg = map[string]string{
168-
"": "%s is not equal to %s",
169-
"=": "%s is not equal to %s",
170-
"!=": "%s is equal to %s",
171-
">": "%s is less than or equal to %s",
172-
"<": "%s is greater than or equal to %s",
173-
">=": "%s is less than %s",
174-
"=>": "%s is less than %s",
175-
"<=": "%s is greater than %s",
176-
"=<": "%s is greater than %s",
177-
"~": "%s does not have same major and minor version as %s",
178-
"~>": "%s does not have same major and minor version as %s",
179-
"^": "%s does not have same major version as %s",
180-
}
181-
182167
ops := make([]string, 0, len(constraintOps))
183168
for k := range constraintOps {
184169
ops = append(ops, regexp.QuoteMeta(k))
@@ -223,7 +208,7 @@ type constraint struct {
223208
}
224209

225210
// Check if a version meets the constraint
226-
func (c *constraint) check(v *Version) bool {
211+
func (c *constraint) check(v *Version) (bool, error) {
227212
return constraintOps[c.origfunc](v, c)
228213
}
229214

@@ -232,7 +217,7 @@ func (c *constraint) string() string {
232217
return c.origfunc + c.orig
233218
}
234219

235-
type cfunc func(v *Version, c *constraint) bool
220+
type cfunc func(v *Version, c *constraint) (bool, error)
236221

237222
func parseConstraint(c string) (*constraint, error) {
238223
if len(c) > 0 {
@@ -301,111 +286,148 @@ func parseConstraint(c string) (*constraint, error) {
301286
}
302287

303288
// Constraint functions
304-
func constraintNotEqual(v *Version, c *constraint) bool {
289+
func constraintNotEqual(v *Version, c *constraint) (bool, error) {
305290
if c.dirty {
306291

307292
// If there is a pre-release on the version but the constraint isn't looking
308293
// for them assume that pre-releases are not compatible. See issue 21 for
309294
// more details.
310295
if v.Prerelease() != "" && c.con.Prerelease() == "" {
311-
return false
296+
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
312297
}
313298

314299
if c.con.Major() != v.Major() {
315-
return true
300+
return true, nil
316301
}
317302
if c.con.Minor() != v.Minor() && !c.minorDirty {
318-
return true
303+
return true, nil
319304
} else if c.minorDirty {
320-
return false
305+
return false, fmt.Errorf("%s is equal to %s", v, c.orig)
321306
} else if c.con.Patch() != v.Patch() && !c.patchDirty {
322-
return true
307+
return true, nil
323308
} else if c.patchDirty {
324309
// Need to handle prereleases if present
325310
if v.Prerelease() != "" || c.con.Prerelease() != "" {
326-
return comparePrerelease(v.Prerelease(), c.con.Prerelease()) != 0
311+
eq := comparePrerelease(v.Prerelease(), c.con.Prerelease()) != 0
312+
if eq {
313+
return true, nil
314+
}
315+
return false, fmt.Errorf("%s is equal to %s", v, c.orig)
327316
}
328-
return false
317+
return false, fmt.Errorf("%s is equal to %s", v, c.orig)
329318
}
330319
}
331320

332-
return !v.Equal(c.con)
321+
eq := v.Equal(c.con)
322+
if eq {
323+
return false, fmt.Errorf("%s is equal to %s", v, c.orig)
324+
}
325+
326+
return true, nil
333327
}
334328

335-
func constraintGreaterThan(v *Version, c *constraint) bool {
329+
func constraintGreaterThan(v *Version, c *constraint) (bool, error) {
336330

337331
// If there is a pre-release on the version but the constraint isn't looking
338332
// for them assume that pre-releases are not compatible. See issue 21 for
339333
// more details.
340334
if v.Prerelease() != "" && c.con.Prerelease() == "" {
341-
return false
335+
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
342336
}
343337

338+
var eq bool
339+
344340
if !c.dirty {
345-
return v.Compare(c.con) == 1
341+
eq = v.Compare(c.con) == 1
342+
if eq {
343+
return true, nil
344+
}
345+
return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig)
346346
}
347347

348348
if v.Major() > c.con.Major() {
349-
return true
349+
return true, nil
350350
} else if v.Major() < c.con.Major() {
351-
return false
351+
return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig)
352352
} else if c.minorDirty {
353353
// This is a range case such as >11. When the version is something like
354354
// 11.1.0 is it not > 11. For that we would need 12 or higher
355-
return false
355+
return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig)
356356
} else if c.patchDirty {
357357
// This is for ranges such as >11.1. A version of 11.1.1 is not greater
358358
// which one of 11.2.1 is greater
359-
return v.Minor() > c.con.Minor()
359+
eq = v.Minor() > c.con.Minor()
360+
if eq {
361+
return true, nil
362+
}
363+
return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig)
360364
}
361365

362366
// If we have gotten here we are not comparing pre-preleases and can use the
363367
// Compare function to accomplish that.
364-
return v.Compare(c.con) == 1
368+
eq = v.Compare(c.con) == 1
369+
if eq {
370+
return true, nil
371+
}
372+
return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig)
365373
}
366374

367-
func constraintLessThan(v *Version, c *constraint) bool {
375+
func constraintLessThan(v *Version, c *constraint) (bool, error) {
368376
// If there is a pre-release on the version but the constraint isn't looking
369377
// for them assume that pre-releases are not compatible. See issue 21 for
370378
// more details.
371379
if v.Prerelease() != "" && c.con.Prerelease() == "" {
372-
return false
380+
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
373381
}
374382

375-
return v.Compare(c.con) < 0
383+
eq := v.Compare(c.con) < 0
384+
if eq {
385+
return true, nil
386+
}
387+
return false, fmt.Errorf("%s is greater than or equal to %s", v, c.orig)
376388
}
377389

378-
func constraintGreaterThanEqual(v *Version, c *constraint) bool {
390+
func constraintGreaterThanEqual(v *Version, c *constraint) (bool, error) {
379391

380392
// If there is a pre-release on the version but the constraint isn't looking
381393
// for them assume that pre-releases are not compatible. See issue 21 for
382394
// more details.
383395
if v.Prerelease() != "" && c.con.Prerelease() == "" {
384-
return false
396+
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
385397
}
386398

387-
return v.Compare(c.con) >= 0
399+
eq := v.Compare(c.con) >= 0
400+
if eq {
401+
return true, nil
402+
}
403+
return false, fmt.Errorf("%s is less than %s", v, c.orig)
388404
}
389405

390-
func constraintLessThanEqual(v *Version, c *constraint) bool {
406+
func constraintLessThanEqual(v *Version, c *constraint) (bool, error) {
391407
// If there is a pre-release on the version but the constraint isn't looking
392408
// for them assume that pre-releases are not compatible. See issue 21 for
393409
// more details.
394410
if v.Prerelease() != "" && c.con.Prerelease() == "" {
395-
return false
411+
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
396412
}
397413

414+
var eq bool
415+
398416
if !c.dirty {
399-
return v.Compare(c.con) <= 0
417+
eq = v.Compare(c.con) <= 0
418+
if eq {
419+
return true, nil
420+
}
421+
return false, fmt.Errorf("%s is greater than %s", v, c.orig)
400422
}
401423

402424
if v.Major() > c.con.Major() {
403-
return false
425+
return false, fmt.Errorf("%s is greater than %s", v, c.orig)
404426
} else if v.Major() == c.con.Major() && v.Minor() > c.con.Minor() && !c.minorDirty {
405-
return false
427+
return false, fmt.Errorf("%s is greater than %s", v, c.orig)
406428
}
407429

408-
return true
430+
return true, nil
409431
}
410432

411433
// ~*, ~>* --> >= 0.0.0 (any)
@@ -414,51 +436,56 @@ func constraintLessThanEqual(v *Version, c *constraint) bool {
414436
// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0, <1.3.0
415437
// ~1.2.3, ~>1.2.3 --> >=1.2.3, <1.3.0
416438
// ~1.2.0, ~>1.2.0 --> >=1.2.0, <1.3.0
417-
func constraintTilde(v *Version, c *constraint) bool {
439+
func constraintTilde(v *Version, c *constraint) (bool, error) {
418440
// If there is a pre-release on the version but the constraint isn't looking
419441
// for them assume that pre-releases are not compatible. See issue 21 for
420442
// more details.
421443
if v.Prerelease() != "" && c.con.Prerelease() == "" {
422-
return false
444+
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
423445
}
424446

425447
if v.LessThan(c.con) {
426-
return false
448+
return false, fmt.Errorf("%s is less than %s", v, c.orig)
427449
}
428450

429451
// ~0.0.0 is a special case where all constraints are accepted. It's
430452
// equivalent to >= 0.0.0.
431453
if c.con.Major() == 0 && c.con.Minor() == 0 && c.con.Patch() == 0 &&
432454
!c.minorDirty && !c.patchDirty {
433-
return true
455+
return true, nil
434456
}
435457

436458
if v.Major() != c.con.Major() {
437-
return false
459+
return false, fmt.Errorf("%s does not have same major version as %s", v, c.orig)
438460
}
439461

440462
if v.Minor() != c.con.Minor() && !c.minorDirty {
441-
return false
463+
return false, fmt.Errorf("%s does not have same major and minor version as %s", v, c.orig)
442464
}
443465

444-
return true
466+
return true, nil
445467
}
446468

447469
// When there is a .x (dirty) status it automatically opts in to ~. Otherwise
448470
// it's a straight =
449-
func constraintTildeOrEqual(v *Version, c *constraint) bool {
471+
func constraintTildeOrEqual(v *Version, c *constraint) (bool, error) {
450472
// If there is a pre-release on the version but the constraint isn't looking
451473
// for them assume that pre-releases are not compatible. See issue 21 for
452474
// more details.
453475
if v.Prerelease() != "" && c.con.Prerelease() == "" {
454-
return false
476+
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
455477
}
456478

457479
if c.dirty {
458480
return constraintTilde(v, c)
459481
}
460482

461-
return v.Equal(c.con)
483+
eq := v.Equal(c.con)
484+
if eq {
485+
return true, nil
486+
}
487+
488+
return false, fmt.Errorf("%s is not equal to %s", v, c.orig)
462489
}
463490

464491
// ^* --> (any)
@@ -470,40 +497,54 @@ func constraintTildeOrEqual(v *Version, c *constraint) bool {
470497
// ^0.0.3 --> >=0.0.3 <0.0.4
471498
// ^0.0 --> >=0.0.0 <0.1.0
472499
// ^0 --> >=0.0.0 <1.0.0
473-
func constraintCaret(v *Version, c *constraint) bool {
500+
func constraintCaret(v *Version, c *constraint) (bool, error) {
474501
// If there is a pre-release on the version but the constraint isn't looking
475502
// for them assume that pre-releases are not compatible. See issue 21 for
476503
// more details.
477504
if v.Prerelease() != "" && c.con.Prerelease() == "" {
478-
return false
505+
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
479506
}
480507

481508
// This less than handles prereleases
482509
if v.LessThan(c.con) {
483-
return false
510+
return false, fmt.Errorf("%s is less than %s", v, c.orig)
484511
}
485512

513+
var eq bool
514+
486515
// ^ when the major > 0 is >=x.y.z < x+1
487516
if c.con.Major() > 0 || c.minorDirty {
488517

489518
// ^ has to be within a major range for > 0. Everything less than was
490519
// filtered out with the LessThan call above. This filters out those
491520
// that greater but not within the same major range.
492-
return v.Major() == c.con.Major()
521+
eq = v.Major() == c.con.Major()
522+
if eq {
523+
return true, nil
524+
}
525+
return false, fmt.Errorf("%s does not have same major version as %s", v, c.orig)
493526
}
494527

495528
// ^ when the major is 0 and minor > 0 is >=0.y.z < 0.y+1
496529
if c.con.Major() == 0 && v.Major() > 0 {
497-
return false
530+
return false, fmt.Errorf("%s does not have same major version as %s", v, c.orig)
498531
}
499532
// If the con Minor is > 0 it is not dirty
500533
if c.con.Minor() > 0 || c.patchDirty {
501-
return v.Minor() == c.con.Minor()
534+
eq = v.Minor() == c.con.Minor()
535+
if eq {
536+
return true, nil
537+
}
538+
return false, fmt.Errorf("%s does not have same minor version as %s. Expected minor versions to match when constraint major version is 0", v, c.orig)
502539
}
503540

504541
// At this point the major is 0 and the minor is 0 and not dirty. The patch
505542
// is not dirty so we need to check if they are equal. If they are not equal
506-
return c.con.Patch() == v.Patch()
543+
eq = c.con.Patch() == v.Patch()
544+
if eq {
545+
return true, nil
546+
}
547+
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)
507548
}
508549

509550
func isX(x string) bool {

0 commit comments

Comments
 (0)