File Coverage

blib/lib/Gherkin/AstBuilder.pm
Criterion Covered Total %
statement 94 170 55.2
branch 20 50 40.0
condition 6 16 37.5
subroutine 22 25 88.0
pod 0 17 0.0
total 142 278 51.0


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