1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
|
package constraints
import (
"fmt"
"strings"
)
// Parse parses a constraint string using a syntax similar to that used by
// npm, Go "dep", Rust's "cargo", etc. Exact compatibility with any of these
// systems is not guaranteed, but instead we aim for familiarity in the choice
// of operators and their meanings. The syntax described here is considered the
// canonical syntax for this package, but a Ruby-style syntax is also offered
// via the function "ParseRubyStyle".
//
// A constraint string is a sequence of selection sets delimited by ||, with
// each selection set being a whitespace-delimited sequence of selections.
// Each selection is then the combination of a matching operator and a boundary
// version. The following is an example of a complex constraint string
// illustrating all of these features:
//
// >=1.0.0 <2.0.0 || 1.0.0-beta1 || =2.0.2
//
// In practice constraint strings are usually simpler than this, but this
// complex example allows us to identify each of the parts by example:
//
// Selection Sets: ">=1.0.0 <2.0.0"
// "1.0.0-beta1"
// "=2.0.2"
// Selections: ">=1.0.0"
// "<2.0.0"
// "1.0.0-beta1"
// "=2.0.2"
// Matching Operators: ">=", "<", "=" are explicit operators
// "1.0.0-beta1" has an implicit "=" operator
// Boundary Versions: "1.0.0", "2.0.0", "1.0.0-beta1", "2.0.2"
//
// A constraint string describes the members of a version set by adding exact
// versions or ranges of versions to that set. A version is in the set if
// any one of the selection sets match that version. A selection set matches
// a version if all of its selections match that version. A selection matches
// a version if the version has the indicated relationship with the given
// boundary version.
//
// In the above example, the first selection set matches all released versions
// whose major segment is 1, since both selections must apply. However, the
// remaining two selection sets describe two specific versions outside of that
// range that are also admitted, in addition to those in the indicated range.
//
// The available matching operators are:
//
// < Less than
// <= Less than or equal
// > Greater than
// >= Greater than or equal
// = Equal
// ! Not equal
// ~ Greater than with implied upper limit (described below)
// ^ Greater than excluding new major releases (described below)
//
// If no operator is specified, the operator is implied to be "equal" for a
// full version specification, or a special additional "match" operator for
// a version containing wildcards as described below.
//
// The "~" matching operator is a shorthand for expressing both a lower and
// upper limit within a single expression. The effect of this operator depends
// on how many segments are specified in the boundary version: if only one
// segment is specified then new minor and patch versions are accepted, whereas
// if two or three segments are specified then only patch versions are accepted.
// For example:
//
// ~1 is equivalent to >=1.0.0 <2.0.0
// ~1.0 is equivalent to >=1.0.0 <1.1.0
// ~1.2 is equivalent to >=1.2.0 <1.3.0
// ~1.2.0 is equivalent to >=1.2.0 <1.3.0
// ~1.2.3 is equivalent to >=1.2.3 <1.3.0
//
// The "^" matching operator is similar to "~" except that it always constrains
// only the major version number. It has an additional special behavior for
// when the major version number is zero: in that case, the minor release
// number is constrained, reflecting the common semver convention that initial
// development releases mark breaking changes by incrementing the minor version.
// For example:
//
// ^1 is equivalent to >=1.0.0 <2.0.0
// ^1.2 is equivalent to >=1.2.0 <2.0.0
// ^1.2.3 is equivalent to >=1.2.3 <2.0.0
// ^0.1.0 is equivalent to >=0.1.0 <0.2.0
// ^0.1.2 is equivalent to >=0.1.2 <0.2.0
//
// The boundary version can contain wildcards for the major, minor or patch
// segments, which are specified using the markers "*", "x", or "X". When used
// in a selection with no explicit operator, these specify the implied "match"
// operator and define ranges with similar meaning to the "~" and "^" operators:
//
// 1.* is equivalent to >=1.0.0 <2.0.0
// 1.*.* is equivalent to >=1.0.0 <2.0.0
// 1.0.* is equivalent to >=1.0.0 <1.1.0
//
// When wildcards are used, the first segment specified as a wildcard implies
// that all of the following segments are also wildcards. A version
// specification like "1.*.2" is invalid, because a wildcard minor version
// implies that the patch version must also be a wildcard.
//
// Wildcards have no special meaning when used with explicit operators, and so
// they are merely replaced with zeros in such cases.
//
// Explicit range syntax using a hyphen creates inclusive upper and lower
// bounds:
//
// 1.0.0 - 2.0.0 is equivalent to >=1.0.0 <=2.0.0
// 1.2.3 - 2.3.4 is equivalent to >=1.2.3 <=2.3.4
//
// Requests of exact pre-release versions with the equals operator have
// no special meaning to the constraint parser, but are interpreted as explicit
// requests for those versions when interpreted by the MeetingConstraints
// function (and related functions) in the "versions" package, in the parent
// directory. Pre-release versions that are not explicitly requested are
// excluded from selection so that e.g. "^1.0.0" will not match a version
// "2.0.0-beta.1".
//
// The result is always a UnionSpec, whose members are IntersectionSpecs
// each describing one selection set. In the common case where a string
// contains only one selection, both the UnionSpec and the IntersectionSpec
// will have only one element and can thus be effectively ignored by the
// caller. (Union and intersection of single sets are both no-op.)
// A valid string must contain at least one selection; if an empty selection
// is to be considered as either "no versions" or "all versions" then this
// special case must be handled by the caller prior to calling this function.
//
// If there are syntax errors or ambiguities in the provided string then an
// error is returned. All errors returned by this function are suitable for
// display to English-speaking end-users, and avoid any Go-specific
// terminology.
func Parse(str string) (UnionSpec, error) {
str = strings.TrimSpace(str)
if str == "" {
return nil, fmt.Errorf("empty specification")
}
// Most constraint strings contain only one selection, so we'll
// allocate under that assumption and re-allocate if needed.
uspec := make(UnionSpec, 0, 1)
ispec := make(IntersectionSpec, 0, 1)
remain := str
for {
var selection SelectionSpec
var err error
selection, remain, err = parseSelection(remain)
if err != nil {
return nil, err
}
remain = strings.TrimSpace(remain)
if len(remain) > 0 && remain[0] == '-' {
// Looks like user wants to make a range expression, so we'll
// look for another selection.
remain = strings.TrimSpace(remain[1:])
if remain == "" {
return nil, fmt.Errorf(`operator "-" must be followed by another version selection to specify the upper limit of the range`)
}
var lower, upper SelectionSpec
lower = selection
upper, remain, err = parseSelection(remain)
remain = strings.TrimSpace(remain)
if err != nil {
return nil, err
}
if lower.Operator != OpUnconstrained {
return nil, fmt.Errorf(`lower bound of range specified with "-" operator must be an exact version`)
}
if upper.Operator != OpUnconstrained {
return nil, fmt.Errorf(`upper bound of range specified with "-" operator must be an exact version`)
}
lower.Operator = OpGreaterThanOrEqual
lower.Boundary = lower.Boundary.ConstrainToZero()
if upper.Boundary.IsExact() {
upper.Operator = OpLessThanOrEqual
} else {
upper.Operator = OpLessThan
upper.Boundary = upper.Boundary.ConstrainToUpperBound()
}
ispec = append(ispec, lower, upper)
} else {
if selection.Operator == OpUnconstrained {
// Select a default operator based on whether the version
// specification contains wildcards.
if selection.Boundary.IsExact() {
selection.Operator = OpEqual
} else {
selection.Operator = OpMatch
}
}
if selection.Operator != OpMatch {
switch selection.Operator {
case OpMatch:
// nothing to do
case OpLessThanOrEqual:
if !selection.Boundary.IsExact() {
selection.Operator = OpLessThan
selection.Boundary = selection.Boundary.ConstrainToUpperBound()
}
case OpGreaterThan:
if !selection.Boundary.IsExact() {
// If "greater than" has an imprecise boundary then we'll
// turn it into a "greater than or equal to" and use the
// upper bound of the boundary, so e.g.:
// >1.*.* means >=2.0.0, because that's greater than
// everything matched by 1.*.*.
selection.Operator = OpGreaterThanOrEqual
selection.Boundary = selection.Boundary.ConstrainToUpperBound()
}
default:
selection.Boundary = selection.Boundary.ConstrainToZero()
}
}
ispec = append(ispec, selection)
}
if len(remain) == 0 {
// All done!
break
}
if remain[0] == ',' {
return nil, fmt.Errorf(`commas are not needed to separate version selections; separate with spaces instead`)
}
if remain[0] == '|' {
if !strings.HasPrefix(remain, "||") {
// User was probably trying for "||", so we'll produce a specialized error
return nil, fmt.Errorf(`single "|" is not a valid operator; did you mean "||" to specify an alternative?`)
}
remain = strings.TrimSpace(remain[2:])
if remain == "" {
return nil, fmt.Errorf(`operator "||" must be followed by another version selection`)
}
// Begin a new IntersectionSpec, added to our single UnionSpec
uspec = append(uspec, ispec)
ispec = make(IntersectionSpec, 0, 1)
}
}
uspec = append(uspec, ispec)
return uspec, nil
}
// parseSelection parses one canon-style selection from the prefix of the
// given string, returning the result along with the remaining unconsumed
// string for the caller to use for further processing.
func parseSelection(str string) (SelectionSpec, string, error) {
raw, remain := scanConstraint(str)
var spec SelectionSpec
if len(str) == len(remain) {
if len(remain) > 0 && remain[0] == 'v' {
// User seems to be trying to use a "v" prefix, like "v1.0.0"
return spec, remain, fmt.Errorf(`a "v" prefix should not be used when specifying versions`)
}
// If we made no progress at all then the selection must be entirely invalid.
return spec, remain, fmt.Errorf("the sequence %q is not valid", remain)
}
switch raw.op {
case "":
// We'll deal with this situation in the caller
spec.Operator = OpUnconstrained
case "=":
spec.Operator = OpEqual
case "!":
spec.Operator = OpNotEqual
case ">":
spec.Operator = OpGreaterThan
case ">=":
spec.Operator = OpGreaterThanOrEqual
case "<":
spec.Operator = OpLessThan
case "<=":
spec.Operator = OpLessThanOrEqual
case "~":
if raw.numCt > 1 {
spec.Operator = OpGreaterThanOrEqualPatchOnly
} else {
spec.Operator = OpGreaterThanOrEqualMinorOnly
}
case "^":
if len(raw.nums[0]) > 0 && raw.nums[0][0] == '0' {
// Special case for major version 0, which is initial development:
// we treat the minor number as if it's the major number.
spec.Operator = OpGreaterThanOrEqualPatchOnly
} else {
spec.Operator = OpGreaterThanOrEqualMinorOnly
}
case "=<":
return spec, remain, fmt.Errorf("invalid constraint operator %q; did you mean \"<=\"?", raw.op)
case "=>":
return spec, remain, fmt.Errorf("invalid constraint operator %q; did you mean \">=\"?", raw.op)
default:
return spec, remain, fmt.Errorf("invalid constraint operator %q", raw.op)
}
if raw.sep != "" {
return spec, remain, fmt.Errorf("no spaces allowed after operator %q", raw.op)
}
if raw.numCt > 3 {
return spec, remain, fmt.Errorf("too many numbered portions; only three are allowed (major, minor, patch)")
}
// Unspecified portions are either zero or wildcard depending on whether
// any explicit wildcards are present.
seenWild := false
for i, s := range raw.nums {
switch {
case isWildcardNum(s):
seenWild = true
case i >= raw.numCt:
if seenWild {
raw.nums[i] = "*"
} else {
raw.nums[i] = "0"
}
default:
// If we find a non-wildcard after we've already seen a wildcard
// then this specification is inconsistent, which is an error.
if seenWild {
return spec, remain, fmt.Errorf("can't use exact %s segment after a previous segment was wildcard", rawNumNames[i])
}
}
}
if seenWild {
if raw.pre != "" {
return spec, remain, fmt.Errorf(`can't use prerelease segment (introduced by "-") in a version with wildcards`)
}
if raw.meta != "" {
return spec, remain, fmt.Errorf(`can't use build metadata segment (introduced by "+") in a version with wildcards`)
}
}
spec.Boundary = raw.VersionSpec()
return spec, remain, nil
}
|