File Coverage

blib/lib/App/SimpleScan/Substitution.pm
Criterion Covered Total %
statement 144 155 92.9
branch 35 46 76.0
condition 4 6 66.6
subroutine 18 19 94.7
pod 4 4 100.0
total 205 230 89.1


line stmt bran cond sub pod time code
1             package App::SimpleScan::Substitution;
2              
3 3     3   86629 use warnings;
  3         6  
  3         104  
4 3     3   17 use strict;
  3         6  
  3         104  
5 3     3   2714 use English qw(-no_match_vars);
  3         10015  
  3         25  
6 3     3   1600 use Carp;
  3         6  
  3         264  
7              
8             our $VERSION = '1.00';
9              
10 3     3   16 use Carp;
  3         4  
  3         134  
11 3     3   2012 use App::SimpleScan::TestSpec;
  3         10  
  3         34  
12 3     3   2280 use App::SimpleScan::Substitution::Line;
  3         10  
  3         24  
13 3     3   3825 use Graph;
  3         478368  
  3         131  
14              
15 3     3   28 use base qw(Class::Accessor::Fast);
  3         3  
  3         6643  
16             __PACKAGE__->mk_accessors( qw(dictionary find_vars_callback
17             insert_value_callback) );
18              
19             # Define base variable detector: any (possibly-nested) angle-bracketed string.
20             # Patterns to extract or >variables< from a string.
21             my $out_angled;
22             $out_angled = qr/ < ( [^<>] | (??{$out_angled}) )* > /x;
23             # open angle-bracket then ...
24             # non-angle chars ...
25             # or ...
26             # another angle-bracketed item ...
27             # if there are any ...
28             # and a close angle-bracket
29             my $in_angled;
30             $in_angled = qr/ > ( [^<>] | (??{$in_angled}) )* < /x;
31             # open angle-bracket then ...
32             # non-angle chars ...
33             # or ...
34             # another angle-bracketed item ...
35             # if there are any ...
36             # and a close angle-bracket
37             my $in_or_out_bracketed = qr/ ($out_angled) | ($in_angled) /x;
38              
39             sub _find_angle_bracketed {
40 71     71   1569 my ($string) = @_;
41 71         90 local $_;
42 71         825 my @artifacted = ( $string =~ /$in_or_out_bracketed/xg ); # match bracketed+artifacts
43 71         124 my @defed = grep { defined $_ } @artifacted;
  160         378  
44 71         92 my @angled = grep { ($_) = /^[<>](.*?)[<>]$/ } @defed;
  80         692  
45 71         260 return @angled;
46             #return grep { ($_) = /^[<>](.*?)[<>]$/ } # true angle-bracketed text
47             # grep { defined $_ } # can include undef items
48             # ( $string =~ /($angle_bracketed)/xg ); # match bracketed+artifacts
49             }
50              
51             sub _insert_value {
52 22     22   1960 my ($string, $variable, $value) = @_;
53 22         23 my $was_inserted;
54 22   66     424 $was_inserted ||= ($string =~ s/<$variable>/$value/gs);
55 22   66     95 $was_inserted ||= ($string =~ s/>$variable
56 22         76 ($was_inserted, $string);
57             }
58              
59              
60             ################################
61             # Basic class methods.
62              
63             # Create object:
64             # empty dictionary, default variable definition.
65             # Update with values from the arg ref if they're there.
66             sub new {
67 8     8 1 9310 my ($class, $arg_ref) = @_;
68 8         18 my $self = {};
69 8         24 bless $self, $class;
70              
71 8         37 $self->dictionary({});
72 8         97 $self->find_vars_callback(\&_find_angle_bracketed);
73 8         66 $self->insert_value_callback(\&_insert_value);
74              
75 8 100       55 if ($arg_ref) {
76 6 100       16 if (ref $arg_ref eq 'HASH') {
77 4 100       16 if (exists $arg_ref->{dictionary}) {
78 2 100       9 if (ref $arg_ref->{dictionary} eq 'HASH') {
79 1         6 $self->dictionary($arg_ref->{dictionary});
80             }
81             else {
82 1         20 croak "'dictionary' must be a hash reference";
83             }
84             }
85 3 100       22 if (exists $arg_ref->{find_vars_callback}) {
86 2 100       10 if (ref $arg_ref->{find_vars_callback} eq 'CODE') {
87 1         5 $self->dictionary($arg_ref->{find_vars_callback});
88             }
89             else {
90 1         14 croak "'find_vars_callback' must be a code reference";
91             }
92             }
93             }
94             else {
95 2         27 croak "Argument to new is not a hash reference";
96             }
97             }
98            
99 4         27 return $self;
100             }
101              
102             # We look at the current line and find substitution which is most deeply nested.
103             sub _deepest_substitution {
104 52     52   925 my($self, $string) = @_;
105            
106             # We absolutely have to localize $_ because we may get called recursively
107             # inside the map() below.
108 52         143 local $_;
109            
110             # Get the variables in this line. We'll need these and all their
111             # dependencies, but nothing else. This speeds up substitution in
112             # a major way because we don't try fruitless variations of variables
113             # that are not (and can't be) substituted into the line.
114             #
115             # First, parse the unique substitutions out of the line.
116            
117             # Extract text corresponding to variables to be substituted.
118 52         144 my @vars = $self->find_vars_callback()->($string);
119              
120             # There seem to be none.
121 52 100       150 return unless @vars;
122            
123             # It's possible that a variable was mentioned multiple times in a line.
124             # Extract only the unique names.
125 20         33 my %unique_vars = map {$_ => 1} @vars;
  33         103  
126              
127             # We only want the simple variables now.
128 20         58 @vars = keys %unique_vars;
129 20         26 local $_;
130              
131 20         25 my %finished;
132 20         46 while (@vars) {
133 39         56 my $current = shift @vars;
134 39         110 my @deeper = $self->_deepest_substitution($current);
135 39 100       78 if (@deeper) {
136 7         24 push @vars, @deeper;
137             }
138             else {
139 32         102 $finished{$current}++;
140             }
141             }
142 20         86 return keys %finished;
143             }
144              
145             # If the current thing has substitutions in it, do them and
146             # return the line objects created by this. Otherwise, just
147             # return the lines as they are.
148             sub expand {
149 2     2 1 613 my($self, @lines) = @_;
150              
151             # No lines; do nothing.
152 2 50       6 return () if @lines == 0;
153              
154             # A single line; find the variables in it and expand them.
155 2 50       11 if (@lines == 1) {
156             # Nothing to do if no vars found.
157 2 50       8 return $lines[0] unless $self->find_vars_callback->($lines[0]);
158              
159 2         12 return map {"$_" } $self->_expand_variables(
  7         41  
160             App::SimpleScan::Substitution::Line->new($lines[0])
161             );
162             }
163             # Multiple lines; do them one at a time and return the results of
164             # doing them all.
165             else {
166 0         0 my @done;
167 0         0 foreach my $line (@lines) {
168 0         0 push @done, $self->_expand($line);
169             }
170 0         0 return map { "$_" } @done;
  0         0  
171             }
172             }
173              
174             # Actually do variable substitutions.
175             sub _expand_variables {
176             # We get a Line object. This object has its own "fixed" dictionary
177             # associated with it: this defines the variables that have already
178             # been expanded once for this Line. We have this because it's possible
179             # that a later expansion may insert one of these variables into the
180             # line again, and we want to have a consistent value for the variable(s)
181             # we've already inserted once.
182 10     10   28 my ($self, @line_objs) = @_;
183              
184             # No objects, no output.
185 10 50       21 return unless @line_objs;
186              
187             # More than one: process each one separately.
188 10 100       26 return map { $self->_expand_variables($_) } @line_objs
  4         15  
189             if @line_objs > 1;
190              
191             # A single line object; process it.
192 9         90 my $line_obj = $line_objs[0];
193              
194             # Clone the dictionary, because we're going to modify it with the
195             # fixed values. This effectively prunes the substitution tree at the
196             # points where we've already done substitutions.
197 9         13 my %dictionary = (%{ $self->dictionary() }, $line_obj->fixed);
  9         25  
198            
199             # Localize the slot that contains the dictionary and replace it
200             # with our newly-constructed, possibly-pruned one.
201 9         31 local($self->{dictionary}) = \%dictionary;
202              
203             # Find the most-deeply-nested substitutions; we need to do those first;
204             # prune out anything that looks like a variable, but isn't (because
205             # there's no value for it in the dictionary).
206 9         33 my @var_names = grep { defined $self->dictionary->{$_} }
  12         91  
207             $self->_deepest_substitution("$line_obj");
208              
209             # We have none.
210 9 50       138 return $line_obj unless @var_names;
211              
212             # What we want to do is to get every possible combination of the
213             # active variables in this line from the dictionary, and substitute
214             # all these into the line.
215             #
216             # Since we have a situation where we don't know how many variables there
217             # are, we can't just code this as a set of nested loops. What we do instead
218             # is map each possible combination into a "combination index": think of it
219             # as the decimal representation of a number in a number system where each
220             # position in this system's representation maps into a specific variable.
221             # The number of possible values for this "place" in the number corresponds
222             # to the number of possible values for thr variable.
223             #
224             # It's easy for us to calculate the number of possible combinations: we
225             # simply multiply the number of possible values of all of the variables,
226             # and we get the maximum possible combination index. We can now iterate
227             # from zero to this maxiumum index, converting the decimal number back into
228             # a number in the combinatorial number system; the representation we get
229             # from doing this exactly maps into the proper indexes into the possible
230             # values for each variable.
231              
232             # Count the number of items for each substitution,
233             # and calculate the maximum combination index from this.
234 9         12 my %item_count_for;
235 9         10 my $max_combination = 1;
236 9         23 for my $var_name (sort @var_names) {
237 12         46 $max_combination *=
238             $item_count_for{$var_name} = () =
239             $self->substitution_value($var_name);
240             }
241              
242             # The done queue gets Line objects that don't expand further;
243             # the expansion queue gets things that expanded at least once
244             # (so they need to be checked again).
245 9         147 my @done_queue;
246             my @expansion_queue;
247              
248 9         21 for my $i (0 .. $max_combination-1) {
249              
250             # Get the values for the variables for the current combination index.
251 14         44 my %current_value_of = $self->_comb_index($i, %item_count_for);
252              
253             # Clone the current line. This keeps the fixed items and copies
254             # the line text.
255 14         221 my $changed_line = $line_obj->clone();
256 14         43 my $string_to_alter = $changed_line->line();
257 14         354 my $a_substitution_happened;
258              
259             # Try to substitute each of the currently variables into the line.
260 14         132 for my $var_name (@var_names) {
261 20         65 my $current_variable_value = $current_value_of{$var_name};
262 20         51 my($did_change, $new_string) =
263             $self->insert_value_callback->($string_to_alter,
264             $var_name,
265             $current_variable_value);
266 20 50       46 if ($did_change) {
267             # Substitution worked. Fix this in the new line object.
268 20         77 $changed_line->fix($var_name, [$current_variable_value]);
269 20         55 $changed_line->line( $string_to_alter = $new_string );
270             }
271             }
272            
273            
274             # Decide which queue to put this object on and put it there.
275 14 100       113 my $proper_queue = ($self->find_vars_callback->($string_to_alter)
276             ? \@expansion_queue
277             : \@done_queue
278             );
279 14         20 push @{ $proper_queue }, $changed_line;
  14         52  
280             }
281              
282 9 100       18 if (@expansion_queue) {
283 3         15 return @done_queue, $self->_expand_variables(@expansion_queue);
284             }
285             else {
286 6         63 return @done_queue;
287             }
288             }
289              
290             sub _comb_index {
291             # this subroutine converts a combination index to a specific set of
292             # values, one for each of the variables in the list.
293 38     38   15008 my($self, $index, %item_counts) = @_;
294 38         119 my @indexes = $self->_comb($index, %item_counts);
295 38         54 my $i = 0;
296 38         43 my %selection_for;
297 38         117 my @ordered_keys = sort keys %item_counts;
298 38         48 local $_; ##no critic
299 38         57 my %base_map_of = map { $_ => $i++ } @ordered_keys;
  116         239  
300 38         68 for my $var (@ordered_keys) {
301 116         583 my $value_ref = $self->substitution_value($var);
302 116 50       660 if (defined $value_ref) {
303 116         194 $selection_for{$var} =
304             $self->substitution_value($var)->[$indexes[$base_map_of{$var}]];
305             }
306             else {
307 0         0 $selection_for{$var} = undef;
308             }
309             }
310 38         461 return %selection_for;
311             }
312              
313             sub _comb {
314             # Convert a combination index into a list of indexes into the
315             # value arrays. We don't try to look up tha values, just calculate
316             # the indexes.
317 62     62   19530 my($self, $index, %item_counts) = @_;
318 62         232 my @base_order = sort keys %item_counts;
319 62         89 my @comb;
320 62         66 my $place = 0;
321              
322             # All indexes must start at zero.
323 62         68 my $number_of_items = scalar keys %item_counts;
324 62         124 foreach my $item (keys %item_counts) {
325 212         487 push @comb, 0;
326             }
327              
328             # convert from base 10 to the derived multi-base number
329             # that maps into the indexes into the possible values.
330 62         331 while ($index) {
331 173         799 $comb[$place] = $index % $item_counts{$base_order[$place]};
332 173         277 $index = int $index/$item_counts{$base_order[$place]};
333 173         546 $place++;
334             }
335 62         251 return @comb;
336             }
337              
338             # setter/getter for substitution data.
339             # - setter needs a name and a list of values.
340             # - getter needs a name, returns a list of values.
341             sub substitution_value {
342 248     248 1 375 my ($self, $pragma_name, @pragma_values) = @_;
343 248 50       466 if (! defined $pragma_name) {
344 0         0 die 'No pragma specified';
345             }
346 248 100       458 if (@pragma_values) {
347 4         12 $self->dictionary->{$pragma_name} = \@pragma_values;
348             }
349             return
350 12         72 wantarray ? ( exists $self->dictionary->{$pragma_name}
351 248 50       757 ? @{$self->dictionary->{$pragma_name}}
    100          
352             : () )
353             : $self->dictionary->{$pragma_name};
354             }
355              
356             sub delete_substitution {
357 0     0 1   my ($self, $substitution_name) = @_;
358 0 0         return unless defined $substitution_name;
359 0           delete $self->dictionary->{$substitution_name};
360 0           return;
361             }
362              
363             1; # Magic true value required at end of module
364             __END__