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 = '10.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   325265 use strict;
  2         4  
  2         57  
30 2     2   8 use warnings;
  2         8  
  2         97  
31              
32 2     2   857 use Cucumber::TagExpressions::Node;
  2         8  
  2         1990  
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   271 my ( $state, $allow_eof ) = @_;
44              
45 237 100       333 if ( length($state->{text}) <= $state->{pos} ) {
46 43 50       78 return if $allow_eof;
47 0         0 die "Unexpected end of string parsing tag expression: $state->{text}";
48             }
49 194         273 return substr( $state->{text}, $state->{pos}++, 1 );
50             }
51              
52             sub _get_token {
53 103     103   1097 my ( $state ) = @_;
54              
55 103 100       178 return delete $state->{saved_token} if defined $state->{saved_token};
56              
57 94         99 my $token = '';
58 94         104 while (1) {
59 233         257 my $char = _consume_char( $state, 1 );
60 233 100       321 return ($token ? $token : undef)
    100          
61             if not defined $char;
62              
63 191 100 100     440 if ( $char =~ m/\s/ ) {
    100          
64 42 100       49 if ( $token ) {
65 41         75 return $token;
66             }
67             else {
68 1         2 next;
69             }
70             }
71             elsif ( $char eq '(' or $char eq ')' ) {
72 10 100       17 if ( $token ) {
73 4         7 _save_token( $state, $char );
74 4         6 return $token;
75             }
76             else {
77 6         21 return $char;
78             }
79             }
80 139 100       161 if ( $char eq "\\" ) {
81 4   100     4 $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         6 $token .= $char;
85             }
86             else {
87 1         8 die qq{Tag expression "$state->{text}" could not be parsed because of syntax error: Illegal escape before "$char".};
88             }
89             }
90             else {
91 135         142 $token .= $char;
92             }
93             }
94             }
95              
96             sub _save_token {
97 9     9   12 my ( $state, $token ) = @_;
98              
99 9         14 $state->{saved_token} = $token;
100             }
101              
102             sub _term_expr {
103 47     47   53 my ( $state ) = @_;
104              
105 47         58 my $token = _get_token( $state );
106              
107 46 100       74 die qq{Tag expression "$state->{text}" could not be parsed because of syntax error: Expected operand.}
108             if not defined $token;
109              
110 44 100       79 if ( $token eq '(' ) {
    100          
111 5         9 my $expr = _expr( $state );
112 4         27 my $token = _get_token( $state );
113              
114 4 50 33     12 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         19 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     101 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         581 return Cucumber::TagExpressions::LiteralNode->new( tag => $token );
130             }
131             }
132              
133             sub _expr {
134 25     25   30 my ( $state ) = @_;
135              
136 25         57 my @terms = ( _term_expr( $state ) );
137 23         2350 while ( my $token = _get_token( $state ) ) {
138 25 100 66     54 if ( not defined $token or $token eq ')' ) {
139 5         11 _save_token( $state, $token );
140 5         28 last;
141             }
142 20 100 100     45 if ( not ( $token eq 'or'
143             or $token eq 'and' ) ) {
144 3         26 die qq{Tag expression "$state->{text}" could not be parsed because of syntax error: Expected operator.}
145             }
146              
147 17         24 my $term = _term_expr( $state );
148 15 100       222 if ( $token eq 'and' ) {
149             # immediately combine _and_ terms
150 7         87 push @terms,
151             Cucumber::TagExpressions::AndNode->new(
152             terms => [ pop(@terms), $term ]
153             );
154             }
155             else {
156             # collect _or_ terms
157 8         15 push @terms, $term;
158             }
159             }
160              
161 18 100       60 if ( scalar(@terms) > 1 ) {
162 8         106 return Cucumber::TagExpressions::OrNode->new(
163             terms => \@terms
164             );
165             }
166             # don't wrap a single-term expression in an Or node
167 10         14 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 159314 my ( $class, $text ) = @_;
179              
180 20 50       82 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         48 my $state = { pos => 0, text => $text, saved_token => undef };
185 20         49 my $expr = _expr( $state );
186              
187 14         943 my $token = _get_token( $state );
188              
189 14 100       25 if ( defined $token ) {
190 1 50       2 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         167 return Cucumber::TagExpressions::ExpressionNode->new(
198             sub_expression => $expr
199             );
200             }
201              
202             1;
203              
204             __END__