Skip to content

Commit 4ed619e

Browse files
committed
Restore the ability to have leading 0's with NewVersion
NewVersion has long been a function that coerces versions into SemVer. StrictNewSemver handles being strict about versions. While leading 0's was restored, this adds an option for package users to force NewVersion to be strict and not accept leading 0's. It does less to coerce a version into a valid one. Signed-off-by: Matt Farina <[email protected]>
1 parent 6fec737 commit 4ed619e

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)