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
|
package Cucumber::TagExpressions;
$Cucumber::TagExpressions::VERSION = '6.1.0';
=head1 NAME
Cucumber::TagExpressions - Tag expression parser
=head1 SYNOPSIS
use Cucumber::TagExpressions;
my $expr = Cucumber::TagExpressions->parse( '@a and @b' );
if ( $expr->evaluate( qw/x y z/ ) ) {
say "The evaluation returned false";
}
=head1 DESCRIPTION
Cucumber tag expressions allow users to define the subset of Gherkin
scenarios they want to run. This library parses the expression and
returns an evaluator object which can be used to test the tags specified
on a scenario against the filter expression.
=head1 CLASS METHODS
=cut
use strict;
use warnings;
use Cucumber::TagExpressions::Node;
sub _expect_token {
my ( $state, $token ) = @_;
my $actual = _get_token( $state );
die "Expecting token '$token' but found '$actual'"
if $token ne $actual;
}
sub _consume_char {
my ( $state, $allow_eof ) = @_;
if ( length($state->{text}) <= $state->{pos} ) {
return if $allow_eof;
die "Unexpected end of string parsing tag expression: $state->{text}";
}
return substr( $state->{text}, $state->{pos}++, 1 );
}
sub _get_token {
my ( $state ) = @_;
return delete $state->{saved_token} if defined $state->{saved_token};
my $token = '';
while (1) {
my $char = _consume_char( $state, 1 );
return ($token ? $token : undef)
if not defined $char;
if ( $char =~ m/\s/ ) {
if ( $token ) {
return $token;
}
else {
next;
}
}
elsif ( $char eq '(' or $char eq ')' ) {
if ( $token ) {
_save_token( $state, $char );
return $token;
}
else {
return $char;
}
}
if ( $char eq "\\" ) {
$char = _consume_char( $state, 1 ) // '<end-of-input>';
if ( $char eq '(' or $char eq ')'
or $char eq "\\" or $char =~ /\s/ ) {
$token .= $char;
}
else {
die qq{Tag expression "$state->{text}" could not be parsed because of syntax error: Illegal escape before "$char".};
}
}
else {
$token .= $char;
}
}
}
sub _save_token {
my ( $state, $token ) = @_;
$state->{saved_token} = $token;
}
sub _term_expr {
my ( $state ) = @_;
my $token = _get_token( $state );
die 'Unexpected end of input parsing tag expression'
if not defined $token;
if ( $token eq '(' ) {
my $expr = _expr( $state );
my $token = _get_token( $state );
if ( not $token or $token ne ')' ) {
die qq{Tag expression "$state->{text}" could not be parsed because of syntax error: Unmatched (.}
}
return $expr;
}
elsif ( $token eq 'not' ) {
return Cucumber::TagExpressions::NotNode->new(
expression => _term_expr( $state )
);
}
else {
if ( $token eq 'and' or $token eq 'or' or $token eq 'not' ) {
die qq{Tag expression "$state->{text}" could not be parsed because of syntax error: Expected operand."};
}
return Cucumber::TagExpressions::LiteralNode->new( tag => $token );
}
}
sub _expr {
my ( $state ) = @_;
my @terms = ( _term_expr( $state ) );
while ( my $token = _get_token( $state ) ) {
if ( not defined $token or $token eq ')' ) {
_save_token( $state, $token );
last;
}
if ( not ( $token eq 'or'
or $token eq 'and' ) ) {
die qq{Tag expression "$state->{text}" could not be parsed because of syntax error: Expected operator.}
}
my $term = _term_expr( $state );
if ( $token eq 'and' ) {
# immediately combine _and_ terms
push @terms,
Cucumber::TagExpressions::AndNode->new(
terms => [ pop(@terms), $term ]
);
}
else {
# collect _or_ terms
push @terms, $term;
}
}
if ( scalar(@terms) > 1 ) {
return Cucumber::TagExpressions::OrNode->new(
terms => \@terms
);
}
# don't wrap a single-term expression in an Or node
return $terms[0];
}
=head2 $class->parse( $expression )
Parses the string specified in C<$expression> and returns a
L<Cucumber::TagExpressions::ExpressionNode> instance.
=cut
sub parse {
my ( $class, $text ) = @_;
return Cucumber::TagExpressions::ExpressionNode->new(
sub_expression => undef
)
if $text =~ /^\s*$/; # match the empty string or space-only string as "constant true"
my $state = { pos => 0, text => $text, saved_token => undef };
my $expr = _expr( $state );
my $token = _get_token( $state );
if ( defined $token ) {
if ( $token eq ')' ) {
die qq{Tag expression "$state->{text}" could not be parsed because of syntax error: Unmatched ).};
}
die "Junk at end of expression: $token";
}
return Cucumber::TagExpressions::ExpressionNode->new(
sub_expression => $expr
);
}
1;
__END__
=head1 LICENSE
Please see the included LICENSE for the canonical version. In summary:
The MIT License (MIT)
Copyright (c) 2021 Erik Huelsmann
Copyright (c) 2021 Cucumber Ltd
This work is loosely derived from prior work of the same library for Ruby,
called C<cucumber-tag-expressions>.
=cut
|