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 = '9.1.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   494048 use strict;
  2         6  
  2         86  
30 2     2   13 use warnings;
  2         6  
  2         154  
31              
32 2     2   1308 use Cucumber::TagExpressions::Node;
  2         10  
  2         3264  
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   324 my ( $state, $allow_eof ) = @_;
44              
45 237 100       439 if ( length($state->{text}) <= $state->{pos} ) {
46 43 50       90 return if $allow_eof;
47 0         0 die "Unexpected end of string parsing tag expression: $state->{text}";
48             }
49 194         418 return substr( $state->{text}, $state->{pos}++, 1 );
50             }
51              
52             sub _get_token {
53 103     103   1136 my ( $state ) = @_;
54              
55 103 100       198 return delete $state->{saved_token} if defined $state->{saved_token};
56              
57 94         123 my $token = '';
58 94         108 while (1) {
59 233         362 my $char = _consume_char( $state, 1 );
60 233 100       449 return ($token ? $token : undef)
    100          
61             if not defined $char;
62              
63 191 100 100     614 if ( $char =~ m/\s/ ) {
    100          
64 42 100       61 if ( $token ) {
65 41         111 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         9 _save_token( $state, $char );
74 4         8 return $token;
75             }
76             else {
77 6         11 return $char;
78             }
79             }
80 139 100       262 if ( $char eq "\\" ) {
81 4   100     9 $char = _consume_char( $state, 1 ) // '';
82 4 100 33     31 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         13 die qq{Tag expression "$state->{text}" could not be parsed because of syntax error: Illegal escape before "$char".};
88             }
89             }
90             else {
91 135         197 $token .= $char;
92             }
93             }
94             }
95              
96             sub _save_token {
97 9     9   12 my ( $state, $token ) = @_;
98              
99 9         15 $state->{saved_token} = $token;
100             }
101              
102             sub _term_expr {
103 47     47   77 my ( $state ) = @_;
104              
105 47         73 my $token = _get_token( $state );
106              
107 46 100       103 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       92 if ( $token eq '(' ) {
    100          
111 5         12 my $expr = _expr( $state );
112 4         28 my $token = _get_token( $state );
113              
114 4 50 33     13 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         20 return $expr;
119             }
120             elsif ( $token eq 'not' ) {
121 5         15 return Cucumber::TagExpressions::NotNode->new(
122             expression => _term_expr( $state )
123             );
124             }
125             else {
126 34 50 33     175 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         863 return Cucumber::TagExpressions::LiteralNode->new( tag => $token );
130             }
131             }
132              
133             sub _expr {
134 25     25   44 my ( $state ) = @_;
135              
136 25         58 my @terms = ( _term_expr( $state ) );
137 23         3224 while ( my $token = _get_token( $state ) ) {
138 25 100 66     76 if ( not defined $token or $token eq ')' ) {
139 5         9 _save_token( $state, $token );
140 5         32 last;
141             }
142 20 100 100     51 if ( not ( $token eq 'or'
143             or $token eq 'and' ) ) {
144 3         39 die qq{Tag expression "$state->{text}" could not be parsed because of syntax error: Expected operator.}
145             }
146              
147 17         46 my $term = _term_expr( $state );
148 15 100       225 if ( $token eq 'and' ) {
149             # immediately combine _and_ terms
150 7         93 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       37 if ( scalar(@terms) > 1 ) {
162 8         98 return Cucumber::TagExpressions::OrNode->new(
163             terms => \@terms
164             );
165             }
166             # don't wrap a single-term expression in an Or node
167 10         16 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 277114 my ( $class, $text ) = @_;
179              
180 20 50       122 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         82 my $state = { pos => 0, text => $text, saved_token => undef };
185 20         53 my $expr = _expr( $state );
186              
187 14         1551 my $token = _get_token( $state );
188              
189 14 100       28 if ( defined $token ) {
190 1 50       5 if ( $token eq ')' ) {
191 1         13 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         218 return Cucumber::TagExpressions::ExpressionNode->new(
198             sub_expression => $expr
199             );
200             }
201              
202             1;
203              
204             __END__