File: canon_style.go

package info (click to toggle)
golang-github-apparentlymart-go-versions 1.0.1-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, sid, trixie
  • size: 292 kB
  • sloc: makefile: 2
file content (352 lines) | stat: -rw-r--r-- 13,164 bytes parent folder | download
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
}