Skip to content

Commit ebda872

Browse files
authored
Merge pull request #266 from mattfarina/restore-calver
Restore the ability to have leading 0's with NewVersion
2 parents 6fec737 + 4ed619e commit ebda872

File tree

2 files changed

+209
-63
lines changed

2 files changed

+209
-63
lines changed

version.go

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@ import (
1616
var versionRegex *regexp.Regexp
1717
var looseVersionRegex *regexp.Regexp
1818

19+
// CoerceNewVersion sets if leading 0's are allowd in the version part. Leading 0's are
20+
// not allowed in a valid semantic version. When set to true, NewVersion will coerce
21+
// leading 0's into a valid version.
22+
var CoerceNewVersion = true
23+
1924
// DetailedNewVersionErrors specifies if detailed errors are returned from the NewVersion
20-
// function. If set to false ErrInvalidSemVer is returned for an invalid version. This does
21-
// not apply to StrictNewVersion. Setting this function to false returns errors more
22-
// quickly.
25+
// function. This is used when CoerceNewVersion is set to false. If set to false
26+
// ErrInvalidSemVer is returned for an invalid version. This does not apply to
27+
// StrictNewVersion. Setting this function to false returns errors more quickly.
2328
var DetailedNewVersionErrors = true
2429

2530
var (
@@ -156,6 +161,9 @@ func StrictNewVersion(v string) (*Version, error) {
156161
// attempts to convert it to SemVer. If you want to validate it was a strict
157162
// semantic version at parse time see StrictNewVersion().
158163
func NewVersion(v string) (*Version, error) {
164+
if CoerceNewVersion {
165+
return coerceNewVersion(v)
166+
}
159167
m := versionRegex.FindStringSubmatch(v)
160168
if m == nil {
161169

@@ -225,6 +233,60 @@ func NewVersion(v string) (*Version, error) {
225233
return sv, nil
226234
}
227235

236+
func coerceNewVersion(v string) (*Version, error) {
237+
m := looseVersionRegex.FindStringSubmatch(v)
238+
if m == nil {
239+
return nil, ErrInvalidSemVer
240+
}
241+
242+
sv := &Version{
243+
metadata: m[8],
244+
pre: m[5],
245+
original: v,
246+
}
247+
248+
var err error
249+
sv.major, err = strconv.ParseUint(m[1], 10, 64)
250+
if err != nil {
251+
return nil, fmt.Errorf("Error parsing version segment: %s", err)
252+
}
253+
254+
if m[2] != "" {
255+
sv.minor, err = strconv.ParseUint(strings.TrimPrefix(m[2], "."), 10, 64)
256+
if err != nil {
257+
return nil, fmt.Errorf("Error parsing version segment: %s", err)
258+
}
259+
} else {
260+
sv.minor = 0
261+
}
262+
263+
if m[3] != "" {
264+
sv.patch, err = strconv.ParseUint(strings.TrimPrefix(m[3], "."), 10, 64)
265+
if err != nil {
266+
return nil, fmt.Errorf("Error parsing version segment: %s", err)
267+
}
268+
} else {
269+
sv.patch = 0
270+
}
271+
272+
// Perform some basic due diligence on the extra parts to ensure they are
273+
// valid.
274+
275+
if sv.pre != "" {
276+
if err = validatePrerelease(sv.pre); err != nil {
277+
return nil, err
278+
}
279+
}
280+
281+
if sv.metadata != "" {
282+
if err = validateMetadata(sv.metadata); err != nil {
283+
return nil, err
284+
}
285+
}
286+
287+
return sv, nil
288+
}
289+
228290
// New creates a new instance of Version with each of the parts passed in as
229291
// arguments instead of parsing a version string.
230292
func New(major, minor, patch uint64, pre, metadata string) *Version {

version_test.go

Lines changed: 144 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -78,75 +78,157 @@ func TestStrictNewVersion(t *testing.T) {
7878
}
7979

8080
func TestNewVersion(t *testing.T) {
81-
tests := []struct {
82-
version string
83-
err bool
84-
}{
85-
{"1.2.3", false},
86-
{"1.2.3-alpha.01", true},
87-
{"1.2.3+test.01", false},
88-
{"1.2.3-alpha.-1", false},
89-
{"v1.2.3", false},
90-
{"1.0", false},
91-
{"v1.0", false},
92-
{"1", false},
93-
{"v1", false},
94-
{"1.2.beta", true},
95-
{"v1.2.beta", true},
96-
{"foo", true},
97-
{"1.2-5", false},
98-
{"v1.2-5", false},
99-
{"1.2-beta.5", false},
100-
{"v1.2-beta.5", false},
101-
{"\n1.2", true},
102-
{"\nv1.2", true},
103-
{"1.2.0-x.Y.0+metadata", false},
104-
{"v1.2.0-x.Y.0+metadata", false},
105-
{"1.2.0-x.Y.0+metadata-width-hypen", false},
106-
{"v1.2.0-x.Y.0+metadata-width-hypen", false},
107-
{"1.2.3-rc1-with-hypen", false},
108-
{"v1.2.3-rc1-with-hypen", false},
109-
{"1.2.3.4", true},
110-
{"v1.2.3.4", true},
111-
{"1.2.2147483648", false},
112-
{"1.2147483648.3", false},
113-
{"2147483648.3.0", false},
114-
115-
// Due to having 4 parts these should produce an error. See
116-
// https://github.com/Masterminds/semver/issues/185 for the reason for
117-
// these tests.
118-
{"12.3.4.1234", true},
119-
{"12.23.4.1234", true},
120-
{"12.3.34.1234", true},
81+
t.Run("With loose NewVersion", func(t *testing.T) {
82+
tests := []struct {
83+
version string
84+
err bool
85+
}{
86+
{"1.2.3", false},
87+
{"1.2.3-alpha.01", true},
88+
{"1.2.3+test.01", false},
89+
{"1.2.3-alpha.-1", false},
90+
{"v1.2.3", false},
91+
{"1.0", false},
92+
{"v1.0", false},
93+
{"1", false},
94+
{"v1", false},
95+
{"1.2.beta", true},
96+
{"v1.2.beta", true},
97+
{"foo", true},
98+
{"1.2-5", false},
99+
{"v1.2-5", false},
100+
{"1.2-beta.5", false},
101+
{"v1.2-beta.5", false},
102+
{"\n1.2", true},
103+
{"\nv1.2", true},
104+
{"1.2.0-x.Y.0+metadata", false},
105+
{"v1.2.0-x.Y.0+metadata", false},
106+
{"1.2.0-x.Y.0+metadata-width-hypen", false},
107+
{"v1.2.0-x.Y.0+metadata-width-hypen", false},
108+
{"1.2.3-rc1-with-hypen", false},
109+
{"v1.2.3-rc1-with-hypen", false},
110+
{"1.2.3.4", true},
111+
{"v1.2.3.4", true},
112+
{"1.2.2147483648", false},
113+
{"1.2147483648.3", false},
114+
{"2147483648.3.0", false},
115+
116+
// Due to having 4 parts these should produce an error. See
117+
// https://github.com/Masterminds/semver/issues/185 for the reason for
118+
// these tests.
119+
{"12.3.4.1234", true},
120+
{"12.23.4.1234", true},
121+
{"12.3.34.1234", true},
122+
123+
// The SemVer spec in a pre-release expects to allow [0-9A-Za-z-].
124+
{"20221209-update-renovatejson-v4", false},
125+
126+
// Various cases that are invalid semver
127+
{"1.1.2+.123", true}, // A leading . in build metadata. This would signify that the first segment is empty
128+
{"1.0.0-alpha_beta", true}, // An underscore in the pre-release is an invalid character
129+
{"1.0.0-alpha..", true}, // Multiple empty segments
130+
{"1.0.0-alpha..1", true}, // Multiple empty segments but one with a value
131+
{"9.8.7+meta+meta", true}, // Multiple metadata parts
132+
{"1.2.31----RC-SNAPSHOT.12.09.1--.12+788", true}, // Leading 0 in a number part of a pre-release segment
133+
134+
// Versions that are invalid but in loose mode are handled.
135+
// This enables a calver-ish style. This pattern has long
136+
// been supported by this package even though it technically
137+
// breaks from semver. Tools built on this allow it.
138+
{"01.1.1", false}, // A leading 0 on a number segment
139+
{"1.01.1", false}, // A leading 0 on a number segment
140+
{"1.1.01", false}, // A leading 0 on a number segment
141+
}
121142

122-
// The SemVer spec in a pre-release expects to allow [0-9A-Za-z-].
123-
{"20221209-update-renovatejson-v4", false},
143+
for _, tc := range tests {
144+
_, err := NewVersion(tc.version)
145+
if tc.err && err == nil {
146+
t.Fatalf("expected error for version: %s", tc.version)
147+
} else if !tc.err && err != nil {
148+
t.Fatalf("error for version %s: %s", tc.version, err)
149+
}
150+
}
151+
})
124152

125-
// Various cases that are invalid semver
126-
{"1.1.2+.123", true}, // A leading . in build metadata. This would signify that the first segment is empty
127-
{"1.0.0-alpha_beta", true}, // An underscore in the pre-release is an invalid character
128-
{"1.0.0-alpha..", true}, // Multiple empty segments
129-
{"1.0.0-alpha..1", true}, // Multiple empty segments but one with a value
130-
{"01.1.1", true}, // A leading 0 on a number segment
131-
{"1.01.1", true}, // A leading 0 on a number segment
132-
{"1.1.01", true}, // A leading 0 on a number segment
133-
{"9.8.7+meta+meta", true}, // Multiple metadata parts
134-
{"1.2.31----RC-SNAPSHOT.12.09.1--.12+788", true}, // Leading 0 in a number part of a pre-release segment
135-
}
153+
t.Run("Without loose NewVersion", func(t *testing.T) {
154+
CoerceNewVersion = false
155+
defer func() {
156+
CoerceNewVersion = true
157+
}()
158+
tests := []struct {
159+
version string
160+
err bool
161+
}{
162+
{"1.2.3", false},
163+
{"1.2.3-alpha.01", true},
164+
{"1.2.3+test.01", false},
165+
{"1.2.3-alpha.-1", false},
166+
{"v1.2.3", false},
167+
{"1.0", false},
168+
{"v1.0", false},
169+
{"1", false},
170+
{"v1", false},
171+
{"1.2.beta", true},
172+
{"v1.2.beta", true},
173+
{"foo", true},
174+
{"1.2-5", false},
175+
{"v1.2-5", false},
176+
{"1.2-beta.5", false},
177+
{"v1.2-beta.5", false},
178+
{"\n1.2", true},
179+
{"\nv1.2", true},
180+
{"1.2.0-x.Y.0+metadata", false},
181+
{"v1.2.0-x.Y.0+metadata", false},
182+
{"1.2.0-x.Y.0+metadata-width-hypen", false},
183+
{"v1.2.0-x.Y.0+metadata-width-hypen", false},
184+
{"1.2.3-rc1-with-hypen", false},
185+
{"v1.2.3-rc1-with-hypen", false},
186+
{"1.2.3.4", true},
187+
{"v1.2.3.4", true},
188+
{"1.2.2147483648", false},
189+
{"1.2147483648.3", false},
190+
{"2147483648.3.0", false},
191+
192+
// Due to having 4 parts these should produce an error. See
193+
// https://github.com/Masterminds/semver/issues/185 for the reason for
194+
// these tests.
195+
{"12.3.4.1234", true},
196+
{"12.23.4.1234", true},
197+
{"12.3.34.1234", true},
198+
199+
// The SemVer spec in a pre-release expects to allow [0-9A-Za-z-].
200+
{"20221209-update-renovatejson-v4", false},
201+
202+
// Various cases that are invalid semver
203+
{"1.1.2+.123", true}, // A leading . in build metadata. This would signify that the first segment is empty
204+
{"1.0.0-alpha_beta", true}, // An underscore in the pre-release is an invalid character
205+
{"1.0.0-alpha..", true}, // Multiple empty segments
206+
{"1.0.0-alpha..1", true}, // Multiple empty segments but one with a value
207+
{"01.1.1", true}, // A leading 0 on a number segment
208+
{"1.01.1", true}, // A leading 0 on a number segment
209+
{"1.1.01", true}, // A leading 0 on a number segment
210+
{"9.8.7+meta+meta", true}, // Multiple metadata parts
211+
{"1.2.31----RC-SNAPSHOT.12.09.1--.12+788", true}, // Leading 0 in a number part of a pre-release segment
212+
}
136213

137-
for _, tc := range tests {
138-
_, err := NewVersion(tc.version)
139-
if tc.err && err == nil {
140-
t.Fatalf("expected error for version: %s", tc.version)
141-
} else if !tc.err && err != nil {
142-
t.Fatalf("error for version %s: %s", tc.version, err)
214+
for _, tc := range tests {
215+
_, err := NewVersion(tc.version)
216+
if tc.err && err == nil {
217+
t.Fatalf("expected error for version: %s", tc.version)
218+
} else if !tc.err && err != nil {
219+
t.Fatalf("error for version %s: %s", tc.version, err)
220+
}
143221
}
144-
}
222+
})
145223
}
146224

147225
// TestNewVersionCheckError checks the returned error for compatibility
148226
func TestNewVersionCheckError(t *testing.T) {
149227
t.Run("With detailed errors", func(t *testing.T) {
228+
CoerceNewVersion = false
229+
defer func() {
230+
CoerceNewVersion = true
231+
}()
150232
tests := []struct {
151233
version string
152234
wantErr error
@@ -211,8 +293,10 @@ func TestNewVersionCheckError(t *testing.T) {
211293

212294
t.Run("Without detailed errors", func(t *testing.T) {
213295
DetailedNewVersionErrors = false
296+
CoerceNewVersion = false
214297
defer func() {
215298
DetailedNewVersionErrors = true
299+
CoerceNewVersion = true
216300
}()
217301

218302
tests := []struct {

0 commit comments

Comments
 (0)