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.1.0';
3 1     1   7 use strict;
  1         2  
  1         30  
4 1     1   3 use warnings;
  1         1  
  1         37  
5 1     1   3 use Scalar::Util qw(reftype);
  1         2  
  1         38  
6              
7 1     1   768 use Cucumber::Messages;
  1         138130  
  1         69  
8              
9 1     1   10 use Gherkin::Exceptions;
  1         1  
  1         19  
10 1     1   548 use Gherkin::AstNode;
  1         2  
  1         351  
11              
12             sub new {
13 3     3 0 8 my $class = shift;
14 3         7 my ($id_generator) = @_;
15              
16 3         7 my $id_counter = 0;
17             my $self = bless {
18             stack => [],
19             comments => [],
20             id_generator => $id_generator // sub {
21 18     18   51 return $id_counter++;
22             },
23 3   33     38 uri => '',
24             }, $class;
25 3         14 $self->reset;
26 3         32 return $self;
27             }
28              
29             # Simple builder sugar
30 39     39 0 147 sub ast_node { Gherkin::AstNode->new( $_[0] ) }
31              
32             sub reset {
33 6     6 0 10 my $self = shift;
34 6         13 my ($uri) = @_;
35 6         15 $self->{'stack'} = [ ast_node('None') ];
36 6         14 $self->{'comments'} = [];
37 6         16 $self->{'uri'} = $uri;
38             }
39              
40             sub current_node {
41 72     72 0 86 my $self = shift;
42 72         262 return $self->{'stack'}->[-1];
43             }
44              
45             sub start_rule {
46 33     33 0 69 my ( $self, $rule_type ) = @_;
47 33         38 push( @{ $self->{'stack'} }, ast_node($rule_type) );
  33         84  
48             }
49              
50             sub end_rule {
51 33     33 0 49 my ( $self, $rule_type ) = @_;
52 33         33 my $node = pop( @{ $self->{'stack'} } );
  33         66  
53 33         68 $self->current_node->add( $node->rule_type, $self->transform_node($node) );
54             }
55              
56             sub build {
57 36     36 0 80 my ( $self, $token ) = @_;
58 36 50       76 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         74 $self->current_node->add( $token->matched_type, $token );
66             }
67             }
68              
69             sub get_result {
70 3     3 0 6 my $self = shift;
71 3         9 return $self->current_node->get_single('GherkinDocument');
72             }
73              
74             sub get_location {
75 21     21 0 41 my ( $self, $token, $column ) = @_;
76              
77 1     1   6 use Carp qw/confess/;
  1         2  
  1         1285  
78 21 50       39 confess "What no token?" unless $token;
79              
80             return Cucumber::Messages::Location->new(
81             line => $token->location->{'line'},
82 21   33     567 column => $column // $token->location->{'column'}
83             );
84             }
85              
86             sub get_tags {
87 9     9 0 13 my ( $self, $node ) = @_;
88              
89 9   50     110 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 33 sub get_description { return ($_[1]->get_single('Description') || '') }
156 9     9 0 20 sub get_steps { return $_[1]->get_items('Step') }
157              
158             sub next_id {
159 18     18 0 23 my $self = shift;
160 18         37 return $self->{'id_generator'}->();
161             }
162              
163             ## no critic (ProhibitExcessComplexity, ProhibitCascadingIfElse)
164             sub transform_node {
165 33     33 0 74 my ( $self, $node ) = @_;
166              
167 33 100       193 if ( $node->rule_type eq 'Step' ) {
    50          
    50          
    100          
    100          
    50          
    50          
    50          
    100          
    100          
168 9         31 my $step_line = $node->get_token('StepLine');
169 9   50     23 my $data_table = $node->get_single('DataTable') || undef;
170 9   50     21 my $doc_string = $node->get_single('DocString') || undef;
171              
172 9         24 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         8 my $background_line = $node->get_token('BackgroundLine');
202 3         10 my $description = $self->get_description($node);
203 3         9 my $steps = $self->get_steps($node);
204              
205 3         8 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         18 my $tags = $self->get_tags($node);
215 6         13 my $scenario_node = $node->get_single('Scenario');
216 6         14 my $scenario_line = $scenario_node->get_token('ScenarioLine');
217 6         12 my $description = $self->get_description($scenario_node);
218 6         16 my $steps = $self->get_steps($scenario_node);
219 6         14 my $examples = $scenario_node->get_items('ExamplesDefinition');
220              
221 6         10 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         8 my $header = $node->get_single('FeatureHeader');
300 3 50       8 unless ($header) {
301 0         0 warn "Missing FeatureHeader!";
302 0         0 return;
303             }
304 3         8 my $feature_line = $header->get_token('FeatureLine');
305 3 50       8 unless ($feature_line) {
306 0         0 warn "Missing FeatureLine";
307 0         0 return;
308             }
309 3         7 my $tags = $self->get_tags($header);
310              
311 3         6 my @children;
312 3         7 my $background = $node->get_single('Background');
313 3 50       8 if ( $background ) {
314 3         56 push @children,
315             Cucumber::Messages::FeatureChild->new(
316             background => $background
317             );
318             }
319             push @children,
320             map {
321 6         167 Cucumber::Messages::FeatureChild->new(
322             scenario => $_
323             )
324 3         1427 } @{ $node->get_items('ScenarioDefinition') };
  3         14  
325             push @children,
326             map {
327 0         0 Cucumber::Messages::FeatureChild->new(
328             rule => $_
329             )
330 3         45 } @{ $node->get_items('Rule') };
  3         14  
331              
332 3         10 my $description = $self->get_description($header);
333 3         15 my $language = $feature_line->matched_gherkin_dialect;
334              
335 3         9 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         12 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         123 comments => $self->{'comments'},
352             ));
353             } else {
354 9         26 return $node;
355             }
356             }
357             ## use critic
358              
359             1;