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