diff options
author | David E. Wheeler <david@justatheory.com> | 2020-05-09 09:22:07 -0400 |
---|---|---|
committer | David E. Wheeler <david@justatheory.com> | 2020-05-09 09:22:07 -0400 |
commit | 28d4413eb16e2b7d47e398faf281393ae7b6f529 (patch) | |
tree | 01987af8683ed1d0d862b4d798f866015b3f1c23 | |
parent | 4fdb22f1ee96c6b3a9feeb9ac0f8ed55fb4c7dcf (diff) | |
parent | 0023a806e96670723c7475687fe3e5ffcfd9a378 (diff) |
Merge branch 'corpus'
Adopt official SemVer regular expressions
Thanks to @jwdonahue for pointing me to it in theory/pg-semver#46. Adopted
here for strict parsing, and modified it for the lenient parsing of
`declare()` and `parse()` (which allow a single integer or a decimal number
version). Resolves #11 and closes #10.
-rw-r--r-- | Changes | 26 | ||||
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | lib/SemVer.pm | 121 | ||||
-rw-r--r-- | t/base.t | 43 | ||||
-rw-r--r-- | t/corpus.t | 112 |
5 files changed, 213 insertions, 93 deletions
@@ -1,9 +1,27 @@ Revision history for Perl extension SemVer. +0.10.0 + - Adopted the official regular expression from the SemVer FAQ for strict + parsing by new(), as well as a modification of that regex for the more + lenient cases supported by declare() and new(). This results in the + following changes in the behavior of the parser: + + SemVers with build metadata but no prerelease are now valid, e.g. + `1.1.2+meta` + + SemVers with a numeric-only prerelease part are no longer valid + if that part has a leading zero, e.g., `1.2.3-123` is valid but + `1.2.3-0123` is not + + Restored support for prerelease and build metadata parts are in + declare() and parse() + - Added tests for the official SemVer test corpus as linked under the + FAQ about an official regular expression: + https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string + - Added explicit boolean overloading to ensure consistent behavior + between different implementations of the version parent class. + 0.7.0 2018-07-24T11:09:17Z - - Implemented Semantic Versioning 2.0.0 specification - - Updated tests for Semantic Versioning 2.0.0 - - added testing for Perl 5.24, 5.26, 5.28 + - Implemented Semantic Versioning 2.0.0 specification + - Updated tests for Semantic Versioning 2.0.0 + - added testing for Perl 5.24, 5.26, 5.28 0.6.0 2015-01-23T05:07:58Z - Removed tests that fail on version.pm 0.9910 and higher due to @@ -31,7 +49,7 @@ Revision history for Perl extension SemVer. 0.3.0 2011-05-26T04:54:50 - Made leading zeros, such as the "04" in "1.04.3" illegal when parsing via `new()`. - - Eliminted "Use of qw(...) as parentheses is deprecated" in the tests + - Eliminated "Use of qw(...) as parentheses is deprecated" in the tests when running on Perl 5.14. 0.2.0 2010-09-17T17:59:57 @@ -1,5 +1,5 @@ -SemVer version 0.7.0 -==================== +SemVer version 0.10.0 +===================== This module subclasses [`version`] to create semantic versions, as defined by the [Semantic Versioning 2.0.0 Specification] diff --git a/lib/SemVer.pm b/lib/SemVer.pm index 97cc321..9a7f3c9 100644 --- a/lib/SemVer.pm +++ b/lib/SemVer.pm @@ -9,30 +9,17 @@ use overload ( '""' => 'stringify', '<=>' => 'vcmp', 'cmp' => 'vcmp', + 'bool' => 'vbool', ); our @ISA = qw(version); -our $VERSION = '0.7.0'; # For Module::Build +our $VERSION = '0.10.0'; # For Module::Build sub _die { require Carp; Carp::croak(@_) } # Prevent version.pm from mucking with our internals. sub import {} -# Adapted from version.pm. -my $STRICT_INTEGER_PART = qr/0|[1-9][0-9]*/; -my $DOT_SEPARATOR = qr/\./; -my $PLUS_SEPARATOR = qr/\+/; -my $DASH_SEPARATOR = qr/-/; -my $STRICT_DOTTED_INTEGER_PART = qr/$DOT_SEPARATOR$STRICT_INTEGER_PART/; -my $STRICT_DOTTED_INTEGER_VERSION = qr/ $STRICT_INTEGER_PART $STRICT_DOTTED_INTEGER_PART{2,} /x; -my $IDENTIFIER = qr/[-0-9A-Za-z]+/; -my $DOTTED_IDENTIFIER = qr/(?:$DOT_SEPARATOR$IDENTIFIER)*/; -my $PRERELEASE = qr/$IDENTIFIER$DOTTED_IDENTIFIER/; -my $METADATA = qr/$IDENTIFIER$DOTTED_IDENTIFIER/; - -my $OPTIONAL_EXTRA_PART = qr/$PRERELEASE($PLUS_SEPARATOR$METADATA)?/; - sub new { my ($class, $ival) = @_; @@ -43,71 +30,64 @@ sub new { if (eval { $ival->isa('version') }) { my $self = $class->SUPER::new($ival); $self->{extra} = $ival->{extra}; - $self->{dash} = $ival->{dash}; - $self->_evalPreRelease($self->{extra}); + $self->{patch} = $ival->{patch}; + $self->{prerelease} = $ival->{prerelease}; return $self; } - my ($val, $dash, $extra) = ( - $ival =~ /^v?($STRICT_DOTTED_INTEGER_VERSION)(?:($DASH_SEPARATOR)($OPTIONAL_EXTRA_PART))?$/ + # Regex taken from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string. + my ($major, $minor, $patch, $prerelease, $meta) = ( + $ival =~ /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/ ); _die qq{Invalid semantic version string format: "$ival"} - unless defined $val; + unless defined $major; - my $self = $class->SUPER::new($val); - $self->{dash} = $dash; - $self->{extra} = $extra; - $self->_evalPreRelease($self->{extra}); - - return $self; + return _init($class->SUPER::new("$major.$minor.$patch"), $prerelease, $meta); } -# Internal function to split up given string into prerelease- and patch-components -sub _evalPreRelease { - no warnings 'uninitialized'; - my $self = shift; - my $v = shift; - my ($preRelease, $plus, $patch) = ( - $v =~ /^($PRERELEASE)(?:($PLUS_SEPARATOR)($METADATA))?$/ - ); - @{$self->{prerelease}} = split $DOT_SEPARATOR,$preRelease; - $self->{plus} = $plus; - @{$self->{patch}} = (split $DOT_SEPARATOR, $patch || undef); - return; +sub _init { + my ($self, $pre, $meta) = @_; + if (defined $pre) { + $self->{extra} = "-$pre"; + @{$self->{prerelease}} = split /[.]/, $pre; + } + if (defined $meta) { + $self->{extra} .= "+$meta"; + @{$self->{patch}} = split /[.]/, $meta; + } + + return $self; } $VERSION = __PACKAGE__->new($VERSION); # For ourselves. -sub declare { - my ($class, $ival) = @_; +sub _lenient { + my ($class, $ctor, $ival) = @_; return $class->new($ival) if Scalar::Util::isvstring($ival) or eval { $ival->isa('version') }; - (my $v = $ival) =~ s/^v?$STRICT_DOTTED_INTEGER_VERSION(?:($DASH_SEPARATOR)($OPTIONAL_EXTRA_PART))[[:space:]]*$//; - my $dash = $1; - my $extra = $2; - $v += 0 if $v =~ s/_//g; # ignore underscores. - my $self = $class->SUPER::declare($v); - $self->{dash} = $dash; - $self->{extra} = $extra; - $self->_evalPreRelease($self->{extra}); - return $self; + # Use official regex for prerelease and meta, use more lenient version num matching and whitespace. + my ($v, $prerelease, $meta) = ( + $ival =~ /^[[:space:]]* + v?([\d_]+(?:\.[\d_]+(?:\.[\d_]+)?)?) + (?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))? + [[:space:]]*$/x + ); + + _die qq{Invalid semantic version string format: "$ival"} + unless defined $v; + + $v += 0 if $v && $v =~ s/_//g; # ignore underscores. + my $code = $class->can("SUPER::$ctor"); + return _init($code->($class, $v), $prerelease, $meta); } -sub parse { - my ($class, $ival) = @_; - return $class->new($ival) if Scalar::Util::isvstring($ival) - or eval { $ival->isa('version') }; +sub declare { + shift->_lenient('declare', @_); +} - (my $v = $ival) =~ s/^v?$STRICT_DOTTED_INTEGER_VERSION(?:($DASH_SEPARATOR)($OPTIONAL_EXTRA_PART))[[:space:]]*$//; - my $dash = $1; - my $extra = $2; - $v += 0 if $v =~ s/_//g; # ignore underscores. - my $self = $class->SUPER::parse($v); - $self->{dash} = $dash; - $self->{extra} = $extra; - $self->_evalPreRelease($self->{extra}); - return $self; +sub parse { + shift->_lenient('parse', @_); } sub stringify { @@ -122,12 +102,15 @@ sub normal { my $self = shift; (my $norm = $self->SUPER::normal) =~ s/^v//; $norm =~ s/_/./g; - return $norm . ($self->{extra} ? "-$self->{extra}" : ''); + return $norm . ($self->{extra} || ''); } sub numify { _die 'Semantic versions cannot be numified'; } sub is_alpha { !!shift->{extra} } - +sub vbool { + my $self = shift; + return version::vcmp($self, $self->declare("0.0.0"), 1); +} # Sort Ordering: # Precedence refers to how versions are compared to each other when ordered. Precedence MUST be calculated by @@ -283,7 +266,7 @@ shown as returned by C<normal()>: ' 012.2.2' | <error> | 12.2.2 | 12.2.2 '1.1' | <error> | 1.1.0 | 1.100.0 1.1 | <error> | 1.1.0 | 1.100.0 - '1.1.0b1' | <error> | 1.1.0-b1 | 1.1.0-b1 + '1.1.0-b1' | 1.1.0-b1 | 1.1.0-b1 | 1.1.0-b1 '1.1-b1' | <error> | 1.1.0-b1 | 1.100.0-b1 '1.2.b1' | <error> | 1.2.0-b1 | 1.2.0-b1 '9.0-beta4' | <error> | 9.0.0-beta4 | 9.0.0-beta4 @@ -378,6 +361,14 @@ Returns true if an ASCII string is appended to the end of the version string. This also means that the version number is a "special version", in the semantic versioning specification meaning of the phrase. +=head3 C<vbool> + + say "Version $semver" if $semver; + say "Not a $semver" if !$semver; + +Returns true for a non-zero semantic semantic version object, without regard +to the prerelease or build metadata parts. Overload boolean operations. + =head3 C<vcmp> Compares the semantic version object to another version object or string and @@ -2,7 +2,7 @@ use strict; use warnings; -use Test::More tests => 610; +use Test::More tests => 666; #use Test::More 'no_plan'; use FindBin qw($Bin); @@ -29,7 +29,6 @@ can_ok $CLASS, qw( # Try the basics. isa_ok my $version = $CLASS->new('0.1.0'), $CLASS, 'An instance'; isa_ok $SemVer::VERSION, $CLASS, q{SemVer's own $VERSION}; -my $is_vpp = !!grep { $_ eq 'version::vpp' } @version::ISA; for my $v (qw( 1.2.2 @@ -45,19 +44,16 @@ for my $v (qw( v1.2.2 999993333.0.0 )) { - isa_ok my $semver =$CLASS->new($v), $CLASS, "new($v)"; + isa_ok my $semver = $CLASS->new($v), $CLASS, "new($v)"; my $str = $v =~ /^v/ ? substr $v, 1 : $v; is "$semver", $str, qq{$v should stringify to "$str"}; $str =~ s/(\d)([a-z].+)$/$1-$2/; is $semver->normal, $str, qq{$v should normalize to "$str"}; - SKIP: { - skip 'Boolean comparison broken with version::vpp', 1, $is_vpp; - if ($v =~ /0\.0\.0/) { - ok !$semver, "$v should be false"; - } else { - ok !!$semver, "$v should be true"; - } + if ($v =~ /0\.0\.0/) { + ok !$semver, "$v should be false"; + } else { + ok !!$semver, "$v should be true"; } ok $semver->is_qv, "$v should be dotted-decimal"; @@ -224,21 +220,27 @@ for my $v (qw( cmp_ok $v, 'eq', $semver, qq{"$v" eq $semver}; } +# Tweak tweak v prefix regex? Some versions of version:vpp do it differently. +my $vq = qr/^\d+[.][^.]+$/; +if ($CLASS->declare('0')->stringify eq 'v0') { + $vq = qr/^\d+([.]?[^.]+)?$/; +} + # Test declare() and parse. for my $spec ( ['1.2.2', '1.2.2'], ['01.2.2', '1.2.2'], ['1.02.2', '1.2.2'], ['1.2.02', '1.2.2'], -# ['1.2.02b', '1.2.2-b'], -# ['1.2.02beta-3 ', '1.2.2-beta-3'], -# ['1.02.02rc1', '1.2.2-rc1'], + ['1.2.02-b', '1.2.2-b'], + ['1.2.02-beta-3 ', '1.2.2-beta-3'], + ['1.02.02-rc1', '1.2.2-rc1'], ['1.0', '1.0.0'], ['1.1', '1.1.0', '1.100.0'], [ 1.1, '1.1.0', '1.100.0'], -# ['1.1b1', '1.1.0-b1', '1.100.0-b1'], -# ['1b', '1.0.0-b'], -# ['9.0beta4', '9.0.0-beta4'], + ['1.1-b1', '1.1.0-b1', '1.100.0-b1'], + ['1-b', '1.0.0-b'], + ['9.0-beta4', '9.0.0-beta4'], [' 012.2.2', '12.2.2'], ['99999998', '99999998.0.0'], ['1.02_30', '1.23.0'], @@ -251,7 +253,7 @@ for my $spec ( ['9', '9.0.0' ], ['0', '0.0.0' ], [0, '0.0.0' ], -# ['0rc1', '0.0.0-rc1' ], + ['0-rc1', '0.0.0-rc1' ], ) { SKIP: { skip 'Two-integer vstrings weak on Perl 5.8', 12 if $no_2digitvst && Scalar::Util::isvstring($spec->[0]); @@ -262,11 +264,8 @@ for my $spec ( $string =~ s/^\s+//; $string =~ s/\s+$//; $string += 0 if $string =~ s/_//g; - my $vstring = $string =~ /^\d+[.][^.]+$/ ? "v$string" : $string; - SKIP: { - skip 'Stringification broken with version::vpp', 1, $is_vpp; - is $l->stringify, $vstring, qq{... And it should stringify to "$vstring"}; - } + my $vstring = $string =~ $vq ? "v$string" : $string; + is $l->stringify, $vstring, qq{... And it should stringify to "$vstring"}; is $l->normal, $spec->[1], qq{... And it should normalize to "$spec->[1]"}; # Compare the non-semantic version string to the semantic one. diff --git a/t/corpus.t b/t/corpus.t new file mode 100644 index 0000000..d5dbcb2 --- /dev/null +++ b/t/corpus.t @@ -0,0 +1,112 @@ +#!/usr/bin/perl -w + +# Test the SemVer corpus from https://regex101.com/r/Ly7O1x/3/. + +use strict; +use warnings; +use Test::More tests => 222; +#use Test::More 'no_plan'; + +use FindBin qw($Bin); +use lib "$Bin/../lib"; +use SemVer; + +# Valid Semantic Versions +for my $v (qw( + 0.0.4 + 1.2.3 + 10.20.30 + 1.1.2-prerelease+meta + 1.1.2+meta + 1.1.2+meta-valid + 1.0.0-alpha + 1.0.0-beta + 1.0.0-alpha.beta + 1.0.0-alpha.beta.1 + 1.0.0-alpha.1 + 1.0.0-alpha0.valid + 1.0.0-alpha.0valid + 1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay + 1.0.0-rc.1+build.1 + 2.0.0-rc.1+build.123 + 1.2.3-beta + 10.2.3-DEV-SNAPSHOT + 1.2.3-SNAPSHOT-123 + 1.0.0 + 2.0.0 + 1.1.7 + 2.0.0+build.1848 + 2.0.1-alpha.1227 + 1.0.0-alpha+beta + 1.2.3----RC-SNAPSHOT.12.9.1--.12+788 + 1.2.3----R-S.12.9.1--.12+meta + 1.2.3----RC-SNAPSHOT.12.9.1--.12 + 1.0.0+0.build.1-rc.10000aaa-kk-0.1 + 1.0.0-0A.is.legal +)) { + local $@; + ok my $sv = eval { SemVer->new($v) }, qq{New "$v" should be valid} or diag $@; + is $sv->stringify, $v, qq{Should stringify to "$v"}; + + ok $sv = eval { SemVer->declare($v) }, qq{Declare "$v" should work} or diag $@; + is $sv->stringify, $v, qq{Should stringify to "$v"}; + + ok $sv = eval { SemVer->parse($v) }, qq{Parse "$v" should work} or diag $@; + is $sv->stringify, $v, qq{Should stringify to "$v"}; +} + +SKIP: { + local $TODO = 'Large versions overflow version.pm integer bounds'; + local $SIG{__WARN__} = sub { }; # Ignore version overflow warning + my $v = '99999999999999999999999.999999999999999999.99999999999999999'; + ok my $sv = eval { SemVer->new($v) }, qq{"$v" should be valid}; + is $sv->stringify, $v, qq{Should stringify to "$v"}; +} + +# Invalid Semantic Versions +for my $bv (qw( + 1 + 1.2 + 1.2.3-0123 + 1.2.3-0123.0123 + 1.1.2+.123 + +invalid + -invalid + -invalid+invalid + -invalid.01 + alpha + alpha.beta + alpha.beta.1 + alpha.1 + alpha+beta + alpha_beta + alpha. + alpha.. + beta + 1.0.0-alpha_beta + -alpha. + 1.0.0-alpha.. + 1.0.0-alpha..1 + 1.0.0-alpha...1 + 1.0.0-alpha....1 + 1.0.0-alpha.....1 + 1.0.0-alpha......1 + 1.0.0-alpha.......1 + 01.1.1 + 1.01.1 + 1.1.01 + 1.2 + 1.2.3.DEV + 1.2-SNAPSHOT + 1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788 + 1.2-RC-SNAPSHOT + -1.0.3-gamma+b7718 + +justmeta + 9.8.7+meta+meta + 9.8.7-whatever+meta+meta + 99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12 +)) { + local $@; + eval { SemVer->new($bv) }; + ok $@, qq{"$bv" should be an invalid semver}; +} |