File Coverage

blib/lib/Gherkin/AstBuilder.pm
Criterion Covered Total %
statement 94 173 54.3
branch 20 50 40.0
condition 6 17 35.2
subroutine 22 25 88.0
pod 0 17 0.0
total 142 282 50.3


line stmt bran cond sub pod time code
1             package Gherkin::AstBuilder;
2             $Gherkin::AstBuilder::VERSION = '39.0.0';
3 1     1   8 use strict;
  1         2  
  1         46  
4 1     1   42 use warnings;
  1         1  
  1         68  
5 1     1   7 use Scalar::Util qw(reftype);
  1         2  
  1         63  
6              
7 1     1   1018 use Cucumber::Messages;
  1         233295  
  1         157  
8              
9 1     1   11 use Gherkin::Exceptions;
  1         3  
  1         31  
10 1     1   881 use Gherkin::AstNode;
  1         3  
  1         675  
11              
12             sub new {
13 3     3 0 9 my $class = shift;
14 3         7 my ($id_generator) = @_;
15              
16 3         9 my $id_counter = 0;
17             my $self = bless {
18             stack => [],
19             comments => [],
20             id_generator => $id_generator // sub {
21 18     18   141 return $id_counter++;
22             },
23 3   33     70 uri => '',
24             }, $class;
25 3         17 $self->reset;
26 3         42 return $self;
27             }
28              
29             # Simple builder sugar
30 39     39 0 185 sub ast_node { Gherkin::AstNode->new( $_[0] ) }
31              
32             sub reset {
33 6     6 0 13 my $self = shift;
34 6         16 my ($uri) = @_;
35 6         19 $self->{'stack'} = [ ast_node('None') ];
36 6         48 $self->{'comments'} = [];
37 6         21 $self->{'uri'} = $uri;
38             }
39              
40             sub current_node {
41 72     72 0 125 my $self = shift;
42 72         325 return $self->{'stack'}->[-1];
43             }
44              
45             sub start_rule {
46 33     33 0 68 my ( $self, $rule_type ) = @_;
47 33         60 push( @{ $self->{'stack'} }, ast_node($rule_type) );
  33         92  
48             }
49              
50             sub end_rule {
51 33     33 0 89 my ( $self, $rule_type ) = @_;
52 33         45 my $node = pop( @{ $self->{'stack'} } );
  33         86  
53 33         90 $self->current_node->add( $node->rule_type, $self->transform_node($node) );
54             }
55              
56             sub build {
57 36     36 0 81 my ( $self, $token ) = @_;
58 36 50       128 if ( $token->matched_type eq 'Comment' ) {
59 0         0 push @{ $self->{'comments'} },
  0         0  
60             Cucumber::Messages::Comment->new(
61             location => $self->get_location($token),
62             text => $token->matched_text
63             );
64             } else {
65 36         131 $self->current_node->add( $token->matched_type, $token );
66             }
67             }
68              
69             sub get_result {
70 3     3 0 7 my $self = shift;
71 3         13 return $self->current_node->get_single('GherkinDocument');
72             }
73              
74             sub get_location {
75 21     21 0 53 my ( $self, $token, $column ) = @_;
76              
77 1     1   11 use Carp qw/confess/;
  1         3  
  1         2231  
78 21 50       51 confess "What no token?" unless $token;
79              
80             return Cucumber::Messages::Location->new(
81             line => $token->location->{'line'},
82 21   33     880 column => $column // $token->location->{'column'}
83             );
84             }
85              
86             sub get_tags {
87 9     9 0 16 my ( $self, $node ) = @_;
88              
89 9   50     116 my $tags_node = $node->get_single('Tags') || return [];
90 0         0 my @tags;
91              
92 0         0 for my $token ( @{ $tags_node->get_tokens('TagLine') } ) {
  0         0  
93 0         0 for my $item ( @{ $token->matched_items } ) {
  0         0  
94             push @tags,
95             Cucumber::Messages::Tag->new(
96             id => $self->next_id,
97             location => $self->get_location( $token, $item->{'column'} ),
98 0         0 name => $item->{'text'}
99             );
100             }
101             }
102              
103 0         0 return \@tags;
104             }
105              
106             sub get_table_rows {
107 0     0 0 0 my ( $self, $node ) = @_;
108 0         0 my @rows;
109              
110 0         0 for my $token ( @{ $node->get_tokens('TableRow') } ) {
  0         0  
111 0         0 push @rows, Cucumber::Messages::TableRow->new(
112             id => $self->next_id,
113             location => $self->get_location($token),
114             cells => $self->get_cells($token)
115             );
116             }
117              
118 0         0 $self->ensure_cell_count( \@rows );
119 0         0 return \@rows;
120             }
121              
122             sub ensure_cell_count {
123 0     0 0 0 my ( $self, $rows ) = @_;
124 0 0       0 return unless @{$rows};
  0         0  
125              
126 0         0 my $cell_count;
127              
128 0         0 for my $row (@{$rows}) {
  0         0  
129 0         0 my $this_row_count = @{ $row->cells };
  0         0  
130 0 0       0 $cell_count = $this_row_count unless defined $cell_count;
131 0 0       0 unless ( $cell_count == $this_row_count ) {
132 0         0 Gherkin::Exceptions::AstBuilder->throw(
133             "inconsistent cell count within the table",
134             $row->location );
135             }
136             }
137             }
138              
139             sub get_cells {
140 0     0 0 0 my ( $self, $table_row_token ) = @_;
141 0         0 my @cells;
142 0         0 for my $cell_item ( @{ $table_row_token->matched_items } ) {
  0         0  
143             push @cells,
144             Cucumber::Messages::TableCell->new(
145             location => $self->get_location(
146             $table_row_token, $cell_item->{'column'}
147             ),
148 0         0 value => $cell_item->{'text'}
149             );
150             }
151              
152 0         0 return \@cells;
153             }
154              
155 12   50 12 0 36 sub get_description { return ($_[1]->get_single('Description') || '') }
156 9     9 0 30 sub get_steps { return $_[1]->get_items('Step') }
157              
158             sub next_id {
159 18     18 0 34 my $self = shift;
160 18         57 return $self->{'id_generator'}->();
161             }
162              
163             ## no critic (ProhibitExcessComplexity, ProhibitCascadingIfElse)
164             sub transform_node {
165 33     33 0 82 my ( $self, $node ) = @_;
166              
167 33 100       321 if ( $node->rule_type eq 'Step' ) {
    50          
    50          
    100          
    100          
    50          
    50          
    50          
    100          
    100          
168 9         63 my $step_line = $node->get_token('StepLine');
169 9   50     22 my $data_table = $node->get_single('DataTable') || undef;
170 9   50     25 my $doc_string = $node->get_single('DocString') || undef;
171              
172 9         30 return Cucumber::Messages::Step->new(
173             id => $self->next_id,
174             location => $self->get_location($step_line),
175             keyword => $step_line->matched_keyword,
176             keyword_type => $step_line->matched_keyword_type,
177             text => $step_line->matched_text,
178             doc_string => $doc_string,
179             data_table => $data_table,
180             );
181             } elsif ( $node->rule_type eq 'DocString' ) {
182 0         0 my $separator_token = $node->get_tokens('DocStringSeparator')->[0];
183 0         0 my $media_type = $separator_token->matched_text;
184 0         0 my $delimiter = $separator_token->matched_keyword;
185 0         0 my $line_tokens = $node->get_tokens('Other');
186 0         0 my $content = join( "\n", map { $_->matched_text } @{$line_tokens} );
  0         0  
  0         0  
187              
188 0 0       0 return Cucumber::Messages::DocString->new(
189             location => $self->get_location($separator_token),
190             content => $content,
191             media_type => ($media_type eq '' ) ? undef : $media_type,
192             delimiter => $delimiter
193             );
194             } elsif ( $node->rule_type eq 'DataTable' ) {
195 0         0 my $rows = $self->get_table_rows($node);
196             return Cucumber::Messages::DataTable->new(
197 0         0 location => $rows->[0]->{'location'},
198             rows => $rows
199             );
200             } elsif ( $node->rule_type eq 'Background' ) {
201 3         12 my $background_line = $node->get_token('BackgroundLine');
202 3         14 my $description = $self->get_description($node);
203 3         14 my $steps = $self->get_steps($node);
204              
205 3         12 return Cucumber::Messages::Background->new(
206             id => $self->next_id,
207             location => $self->get_location($background_line),
208             keyword => $background_line->matched_keyword,
209             name => $background_line->matched_text,
210             description => $description,
211             steps => $steps
212             );
213             } elsif ( $node->rule_type eq 'ScenarioDefinition' ) {
214 6         27 my $tags = $self->get_tags($node);
215 6         19 my $scenario_node = $node->get_single('Scenario');
216 6         19 my $scenario_line = $scenario_node->get_token('ScenarioLine');
217 6         21 my $description = $self->get_description($scenario_node);
218 6         21 my $steps = $self->get_steps($scenario_node);
219 6         15 my $examples = $scenario_node->get_items('ExamplesDefinition');
220              
221 6         17 return Cucumber::Messages::Scenario->new(
222             id => $self->next_id,
223             tags => $tags,
224             location => $self->get_location($scenario_line),
225             keyword => $scenario_line->matched_keyword,
226             name => $scenario_line->matched_text,
227             description => $description,
228             steps => $steps,
229             examples => $examples
230             );
231             } elsif ( $node->rule_type eq 'Rule' ) {
232 0         0 my $header = $node->get_single('RuleHeader');
233 0 0       0 unless ($header) {
234 0         0 warn "Missing RuleHeader!";
235 0         0 return;
236             }
237 0         0 my $rule_line = $header->get_token('RuleLine');
238 0 0       0 unless ($rule_line) {
239 0         0 warn "Missing RuleLine";
240 0         0 return;
241             }
242 0         0 my $tags = $self->get_tags($header);
243              
244 0         0 my @children;
245 0         0 my $background = $node->get_single('Background');
246 0 0       0 if ( $background ) {
247 0         0 push @children,
248             Cucumber::Messages::RuleChild->new(
249             background => $background
250             );
251             }
252             push @children, (
253             map {
254 0         0 Cucumber::Messages::RuleChild->new(
255             scenario => $_
256             )
257 0         0 } @{ $node->get_items('ScenarioDefinition') }
  0         0  
258             );
259              
260 0         0 my $description = $self->get_description($header);
261              
262 0         0 return Cucumber::Messages::Rule->new(
263             id => $self->next_id,
264             tags => $tags,
265             location => $self->get_location($rule_line),
266             keyword => $rule_line->matched_keyword,
267             name => $rule_line->matched_text,
268             description => $description,
269             children => \@children
270             );
271             } elsif ( $node->rule_type eq 'ExamplesDefinition' ) {
272 0         0 my $examples_node = $node->get_single('Examples');
273 0         0 my $examples_line = $examples_node->get_token('ExamplesLine');
274 0         0 my $description = $self->get_description($examples_node);
275 0         0 my $examples_table = $examples_node->get_single('ExamplesTable');
276 0 0       0 my $rows =
277             $examples_table ? $self->get_table_rows($examples_table) : undef;
278 0         0 my $tags = $self->get_tags($node);
279              
280             return Cucumber::Messages::Examples->new(
281             id => $self->next_id,
282             tags => $tags,
283             location => $self->get_location($examples_line),
284             keyword => $examples_line->matched_keyword,
285             name => $examples_line->matched_text,
286             description => $description,
287 0 0       0 table_header => ($rows ? shift @{ $rows } : undef),
  0 0       0  
288             table_body => ($rows ? $rows : [])
289             );
290             } elsif ( $node->rule_type eq 'Description' ) {
291 0         0 my @description = @{ $node->get_tokens('Other') };
  0         0  
292              
293             # Trim trailing empty lines
294             pop @description
295 0   0     0 while ( @description && !$description[-1]->matched_text );
296              
297 0         0 return join "\n", map { $_->matched_text } @description;
  0         0  
298             } elsif ( $node->rule_type eq 'Feature' ) {
299 3         11 my $header = $node->get_single('FeatureHeader');
300 3 50       12 unless ($header) {
301 0         0 warn "Missing FeatureHeader!";
302 0         0 return;
303             }
304 3         11 my $feature_line = $header->get_token('FeatureLine');
305 3 50       9 unless ($feature_line) {
306 0         0 warn "Missing FeatureLine";
307 0         0 return;
308             }
309 3         11 my $tags = $self->get_tags($header);
310              
311 3         7 my @children;
312 3         12 my $background = $node->get_single('Background');
313 3 50       12 if ( $background ) {
314 3         81 push @children,
315             Cucumber::Messages::FeatureChild->new(
316             background => $background
317             );
318             }
319             push @children,
320             map {
321 6         198 Cucumber::Messages::FeatureChild->new(
322             scenario => $_
323             )
324 3         1684 } @{ $node->get_items('ScenarioDefinition') };
  3         16  
325             push @children,
326             map {
327 0         0 Cucumber::Messages::FeatureChild->new(
328             rule => $_
329             )
330 3         55 } @{ $node->get_items('Rule') };
  3         11  
331              
332 3         12 my $description = $self->get_description($header);
333 3         15 my $language = $feature_line->matched_gherkin_dialect;
334              
335 3         13 return Cucumber::Messages::Feature->new(
336             tags => $tags,
337             location => $self->get_location($feature_line),
338             language => $language,
339             keyword => $feature_line->matched_keyword,
340             name => $feature_line->matched_text,
341             description => $description,
342             children => \@children
343             );
344             } elsif ( $node->rule_type eq 'GherkinDocument' ) {
345 3         13 my $feature = $node->get_single('Feature');
346              
347             return Cucumber::Messages::Envelope->new(
348             gherkin_document => Cucumber::Messages::GherkinDocument->new(
349             uri => $self->{'uri'},
350             feature => $feature,
351 3         103 comments => $self->{'comments'},
352             ));
353             } else {
354 9         37 return $node;
355             }
356             }
357             ## use critic
358              
359             1;