File Coverage

blib/lib/Cucumber/TagExpressions.pm
Criterion Covered Total %
statement 70 77 90.9
branch 41 48 85.4
condition 19 29 65.5
subroutine 9 10 90.0
pod 1 1 100.0
total 140 165 84.8


line stmt bran cond sub pod time code
1              
2             package Cucumber::TagExpressions;
3             $Cucumber::TagExpressions::VERSION = '6.0.0';
4             =head1 NAME
5              
6             Cucumber::TagExpressions - Tag expression parser
7              
8             =head1 SYNOPSIS
9              
10             use Cucumber::TagExpressions;
11              
12             my $expr = Cucumber::TagExpressions->parse( '@a and @b' );
13             if ( $expr->evaluate( qw/x y z/ ) ) {
14             say "The evaluation returned false";
15             }
16              
17              
18             =head1 DESCRIPTION
19              
20             Cucumber tag expressions allow users to define the subset of Gherkin
21             scenarios they want to run. This library parses the expression and
22             returns an evaluator object which can be used to test the tags specified
23             on a scenario against the filter expression.
24              
25             =head1 CLASS METHODS
26              
27             =cut
28              
29 2     2   409676 use strict;
  2         11  
  2         52  
30 2     2   10 use warnings;
  2         4  
  2         46  
31              
32 2     2   796 use Cucumber::TagExpressions::Node;
  2         40  
  2         1894  
33              
34             sub _expect_token {
35 0     0   0 my ( $state, $token ) = @_;
36              
37 0         0 my $actual = _get_token( $state );
38 0 0       0 die "Expecting token '$token' but found '$actual'"
39             if $token ne $actual;
40             }
41              
42             sub _consume_char {
43 237     237   334 my ( $state, $allow_eof ) = @_;
44              
45 237 100       422 if ( length($state->{text}) <= $state->{pos} ) {
46 43 50       93 return if $allow_eof;
47 0         0 die "Unexpected end of string parsing tag expression: $state->{text}";
48             }
49 194         359 return substr( $state->{text}, $state->{pos}++, 1 );
50             }
51              
52             sub _get_token {
53 103     103   1076 my ( $state ) = @_;
54              
55 103 100       188 return delete $state->{saved_token} if defined $state->{saved_token};
56              
57 94         125 my $token = '';
58 94         115 while (1) {
59 233         295 my $char = _consume_char( $state, 1 );
60 233 100       408 return ($token ? $token : undef)
    100          
61             if not defined $char;
62              
63 191 100 100     562 if ( $char =~ m/\s/ ) {
    100          
64 42 100       57 if ( $token ) {
65 41         88 return $token;
66             }
67             else {
68 1         2 next;
69             }
70             }
71             elsif ( $char eq '(' or $char eq ')' ) {
72 10 100       16 if ( $token ) {
73 4         12 _save_token( $state, $char );
74 4         6 return $token;
75             }
76             else {
77 6         13 return $char;
78             }
79             }
80 139 100       206 if ( $char eq "\\" ) {
81 4   100     8 $char = _consume_char( $state, 1 ) // '';
82 4 100 33     30 if ( $char eq '(' or $char eq ')'
      66        
      100        
83             or $char eq "\\" or $char =~ /\s/ ) {
84 3         4 $token .= $char;
85             }
86             else {
87 1         9 die qq{Tag expression "$state->{text}" could not be parsed because of syntax error: Illegal escape before "$char".};
88             }
89             }
90             else {
91 135         192 $token .= $char;
92             }
93             }
94             }
95              
96             sub _save_token {
97 9     9   12 my ( $state, $token ) = @_;
98              
99 9         18 $state->{saved_token} = $token;
100             }
101              
102             sub _term_expr {
103 47     47   68 my ( $state ) = @_;
104              
105 47         64 my $token = _get_token( $state );
106              
107 46 100       87 die 'Unexpected end of input parsing tag expression'
108             if not defined $token;
109              
110 44 100       77 if ( $token eq '(' ) {
    100          
111 5         11 my $expr = _expr( $state );
112 4         36 my $token = _get_token( $state );
113              
114 4 50 33     15 if ( not $token or $token ne ')' ) {
115 0         0 die qq{Tag expression "$state->{text}" could not be parsed because of syntax error: Unmatched (.}
116             }
117              
118 4         22 return $expr;
119             }
120             elsif ( $token eq 'not' ) {
121 5         13 return Cucumber::TagExpressions::NotNode->new(
122             expression => _term_expr( $state )
123             );
124             }
125             else {
126 34 50 33     131 if ( $token eq 'and' or $token eq 'or' or $token eq 'not' ) {
      33        
127 0         0 die qq{Tag expression "$state->{text}" could not be parsed because of syntax error: Expected operand."};
128             }
129 34         618 return Cucumber::TagExpressions::LiteralNode->new( tag => $token );
130             }
131             }
132              
133             sub _expr {
134 25     25   33 my ( $state ) = @_;
135              
136 25         42 my @terms = ( _term_expr( $state ) );
137 23         2376 while ( my $token = _get_token( $state ) ) {
138 25 100 66     71 if ( not defined $token or $token eq ')' ) {
139 5         12 _save_token( $state, $token );
140 5         6 last;
141             }
142 20 100 100     49 if ( not ( $token eq 'or'
143             or $token eq 'and' ) ) {
144 3         25 die qq{Tag expression "$state->{text}" could not be parsed because of syntax error: Expected operator.}
145             }
146              
147 17         26 my $term = _term_expr( $state );
148 15 100       261 if ( $token eq 'and' ) {
149             # immediately combine _and_ terms
150 7         92 push @terms,
151             Cucumber::TagExpressions::AndNode->new(
152             terms => [ pop(@terms), $term ]
153             );
154             }
155             else {
156             # collect _or_ terms
157 8         18 push @terms, $term;
158             }
159             }
160              
161 18 100       37 if ( scalar(@terms) > 1 ) {
162 8         111 return Cucumber::TagExpressions::OrNode->new(
163             terms => \@terms
164             );
165             }
166             # don't wrap a single-term expression in an Or node
167 10         19 return $terms[0];
168             }
169              
170             =head2 $class->parse( $expression )
171              
172             Parses the string specified in C<$expression> and returns a
173             L instance.
174              
175             =cut
176              
177             sub parse {
178 20     20 1 2836 my ( $class, $text ) = @_;
179              
180 20 50       84 return Cucumber::TagExpressions::ExpressionNode->new(
181             sub_expression => undef
182             )
183             if $text =~ /^\s*$/; # match the empty string or space-only string as "constant true"
184 20         63 my $state = { pos => 0, text => $text, saved_token => undef };
185 20         40 my $expr = _expr( $state );
186              
187 14         918 my $token = _get_token( $state );
188              
189 14 100       27 if ( defined $token ) {
190 1 50       3 if ( $token eq ')' ) {
191 1         10 die qq{Tag expression "$state->{text}" could not be parsed because of syntax error: Unmatched ).};
192             }
193              
194 0         0 die "Junk at end of expression: $token";
195             }
196              
197 13         185 return Cucumber::TagExpressions::ExpressionNode->new(
198             sub_expression => $expr
199             );
200             }
201              
202             1;
203              
204             __END__