File Coverage

blib/lib/Perl/ToPerl6/Annotation.pm
Criterion Covered Total %
statement 23 113 20.3
branch 0 38 0.0
condition 0 29 0.0
subroutine 8 19 42.1
pod 8 8 100.0
total 39 207 18.8


line stmt bran cond sub pod time code
1             package Perl::ToPerl6::Annotation;
2              
3 1     1   17 use 5.006001;
  1         3  
4 1     1   5 use strict;
  1         1  
  1         18  
5 1     1   5 use warnings;
  1         1  
  1         24  
6              
7 1     1   4 use Carp qw(confess);
  1         2  
  1         77  
8 1     1   5 use English qw(-no_match_vars);
  1         1  
  1         8  
9              
10 1     1   537 use Perl::ToPerl6::TransformerFactory;
  1         2  
  1         10  
11 1     1   7 use Perl::ToPerl6::Utils qw(:characters hashify);
  1         3  
  1         66  
12 1     1   221 use Readonly;
  1         2  
  1         1252  
13              
14             #-----------------------------------------------------------------------------
15              
16             Readonly::Scalar my $LAST_ELEMENT => -1;
17              
18             #=============================================================================
19             # CLASS methods
20              
21             sub create_annotations {
22 0     0 1   my ($class, $doc) = @_;
23              
24 0           my @annotations = ();
25 0   0       my $comment_elements_ref = $doc->find('PPI::Token::Comment') || return;
26 0           my $annotation_rx = qr{\A (?: [#]! .*? )? \s* [#][#] \s* no \s+ mogrify}xms;
27 0           for my $annotation_element ( grep { $_ =~ $annotation_rx } @{$comment_elements_ref} ) {
  0            
  0            
28 0           push @annotations, Perl::ToPerl6::Annotation->new( -element => $annotation_element);
29             }
30              
31 0           return @annotations;
32             }
33              
34             #-----------------------------------------------------------------------------
35              
36             sub new {
37 0     0 1   my ($class, @args) = @_;
38 0           my $self = bless {}, $class;
39 0           $self->_init(@args);
40 0           return $self;
41             }
42              
43             #=============================================================================
44             # OBJECT methods
45              
46             sub _init {
47 0     0     my ($self, %args) = @_;
48 0   0       my $annotation_element = $args{-element} || confess '-element argument is required';
49 0           $self->{_element} = $annotation_element;
50              
51 0           my %disabled_transformers = _parse_annotation( $annotation_element );
52 0 0         $self->{_disables_all_transformers} = %disabled_transformers ? 0 : 1;
53 0           $self->{_disabled_transformers} = \%disabled_transformers;
54              
55             # Grab surrounding nodes to determine the context.
56             # This determines whether the annotation applies to
57             # the current line or the block that follows.
58 0           my $annotation_line = $annotation_element->logical_line_number();
59 0           my $parent = $annotation_element->parent();
60 0 0         my $grandparent = $parent ? $parent->parent() : undef;
61              
62             # Handle case when it appears on the shebang line. In this
63             # situation, it only affects the first line, not the whole doc
64 0 0         if ( $annotation_element =~ m{\A [#]!}xms) {
65 0           $self->{_effective_range} = [$annotation_line, $annotation_line];
66 0           return $self;
67             }
68              
69             # Handle single-line usage on simple statements. In this
70             # situation, it only affects the line that it appears on.
71 0 0         if ( _is_single_line_annotation_on_simple_statement( $annotation_element )
72             ) {
73 0           $self->{_effective_range} = [$annotation_line, $annotation_line];
74 0           return $self;
75             }
76              
77             # Handle single-line usage on compound statements. In this
78             # situation -- um -- I'm not sure how this works, but it does.
79 0 0         if ( ref $parent eq 'PPI::Structure::Block' ) {
80 0 0 0       if ( ref $grandparent eq 'PPI::Statement::Compound'
81             || ref $grandparent eq 'PPI::Statement::Sub' ) {
82 0 0         if ( $parent->logical_line_number() == $annotation_line ) {
83 0           my $grandparent_line = $grandparent->logical_line_number();
84 0           $self->{_effective_range} = [$grandparent_line, $grandparent_line];
85 0           return $self;
86             }
87             }
88             }
89              
90              
91             # Handle multi-line usage. This is either a "no mogrify" ..
92             # "use mogrify" region or a block where "no mogrify" is in effect
93             # until the end of the scope. The start is the always the "no
94             # mogrify" which we already found. So now we have to search for the end.
95 0           my $end = $annotation_element;
96 0           my $use_mogrify = qr{\A \s* [#][#] \s* use \s+ mogrify}xms;
97              
98             SIB:
99 0           while ( my $esib = $end->next_sibling() ) {
100 0           $end = $esib; # keep track of last sibling encountered in this scope
101 0 0 0       last SIB if $esib->isa('PPI::Token::Comment') && $esib =~ $use_mogrify;
102             }
103              
104             # PPI parses __END__ as a PPI::Statement::End, and everything following is
105             # a child of that statement. That means if we encounter an __END__, we
106             # need to descend into it and continue the analysis.
107 0 0 0       if ( $end->isa( 'PPI::Statement::End' ) and my $kid = $end->child( 0 ) ) {
108 0           $end = $kid;
109             SIB:
110 0           while ( my $esib = $end->next_sibling() ) {
111 0           $end = $esib;
112 0 0 0       last SIB if $esib->isa( 'PPI::Token::Comment' ) &&
113             $esib->content() =~ $use_mogrify;
114             }
115             }
116              
117             # We either found an end or hit the end of the scope.
118 0           my $ending_line = $end->logical_line_number();
119 0           $self->{_effective_range} = [$annotation_line, $ending_line];
120 0           return $self;
121             }
122              
123             #-----------------------------------------------------------------------------
124              
125             sub element {
126 0     0 1   my ($self) = @_;
127 0           return $self->{_element};
128             }
129              
130             #-----------------------------------------------------------------------------
131              
132             sub effective_range {
133 0     0 1   my $self = shift;
134 0           return @{ $self->{_effective_range} };
  0            
135             }
136              
137             #-----------------------------------------------------------------------------
138              
139             sub disabled_transformers {
140 0     0 1   my $self = shift;
141 0           return keys %{ $self->{_disabled_transformers} };
  0            
142             }
143              
144             #-----------------------------------------------------------------------------
145              
146             sub disables_transformer {
147 0     0 1   my ($self, $transformer_name) = @_;
148 0 0         return 1 if $self->{_disabled_transformers}->{$transformer_name};
149 0 0         return 1 if $self->disables_all_transformers();
150 0           return 0;
151             }
152              
153             #-----------------------------------------------------------------------------
154              
155             sub disables_all_transformers {
156 0     0 1   my ($self) = @_;
157 0           return $self->{_disables_all_transformers};
158             }
159              
160             #-----------------------------------------------------------------------------
161              
162             sub disables_line {
163 0     0 1   my ($self, $line_number) = @_;
164 0           my $effective_range = $self->{_effective_range};
165 0 0 0       return 1 if $line_number >= $effective_range->[0]
166             and $line_number <= $effective_range->[$LAST_ELEMENT];
167 0           return 0;
168             }
169              
170             #-----------------------------------------------------------------------------
171              
172             # Recognize a single-line annotation on a simple statement.
173             sub _is_single_line_annotation_on_simple_statement {
174 0     0     my ( $annotation_element ) = @_;
175 0           my $annotation_line = $annotation_element->logical_line_number();
176              
177             # If there is no sibling, we are clearly not a single-line annotation of
178             # any sort.
179 0 0         my $sib = $annotation_element->sprevious_sibling()
180             or return 0;
181              
182             # The easy case: the sibling (whatever it is) is on the same line as the
183             # annotation.
184 0 0         $sib->logical_line_number() == $annotation_line
185             and return 1;
186              
187             # If the sibling is a node, we may have an annotation on one line of a
188             # statement that was split over multiple lines. So we descend through the
189             # children, keeping the last significant child of each, until we bottom
190             # out. If the ultimate significant descendant is on the same line as the
191             # annotation, we accept the annotation as a single-line annotation.
192 0 0 0       if ( $sib->isa( 'PPI::Node' ) &&
193             $sib->logical_line_number() < $annotation_line
194             ) {
195 0           my $neighbor = $sib;
196 0   0       while ( $neighbor->isa( 'PPI::Node' )
197             and my $kid = $neighbor->schild( $LAST_ELEMENT ) ) {
198 0           $neighbor = $kid;
199             }
200 0 0 0       if ( $neighbor &&
201             $neighbor->logical_line_number() == $annotation_line
202             ) {
203 0           return 1;
204             }
205             }
206              
207             # We do not understand any other sort of single-line annotation. Accepting
208             # the annotation as such (if it is) is Someone Else's Problem.
209 0           return 0;
210             }
211              
212             #-----------------------------------------------------------------------------
213              
214             sub _parse_annotation {
215              
216 0     0     my ($annotation_element) = @_;
217              
218             #############################################################################
219             # This regex captures the list of Transformer name patterns that are to be
220             # disabled. It is generally assumed that the element has already been
221             # verified as a no-mogrify annotation. So if this regex does not match,
222             # then it implies that all Transformers are to be disabled.
223             #
224 0           my $no_mogrify = qr{\#\# \s* no \s+ mogrify \s* (?:qw)? [("'] ([\s\w:,]+) }xms;
225             # -------------------------- ------- ----- -----------
226             # | | | |
227             # "## no mogrify" with optional spaces | | |
228             # | | |
229             # Transformer list may be prefixed with "qw" | |
230             # | |
231             # Optional Transformer list must begin with one of these |
232             # |
233             # Capture entire Transformer list (with delimiters) here
234             #
235             #############################################################################
236              
237 0           my @disabled_transformer_names = ();
238 0 0         if ( my ($patterns_string) = $annotation_element =~ $no_mogrify ) {
239              
240             # Compose the specified modules into a regex alternation. Wrap each
241             # in a no-capturing group to permit "|" in the modules specification.
242              
243 0           my @transformer_name_patterns = grep { $_ ne $EMPTY }
  0            
244             split m{\s *[,\s] \s*}xms, $patterns_string;
245 0           my $re = join $PIPE, map {"(?:$_)"} @transformer_name_patterns;
  0            
246 0           my @site_transformer_names = Perl::ToPerl6::TransformerFactory::site_transformer_names();
247 0           @disabled_transformer_names = grep {m/$re/ixms} @site_transformer_names;
  0            
248              
249             # It is possible that the Transformer patterns listed in the annotation do not
250             # match any of the site transformer names. This could happen when running
251             # on a machine that does not have the same set of Transformers as the
252             # author.
253             # So we must return something here, otherwise all Transformers will be
254             # disabled. We probably need to add a mechanism to (optionally) warn
255             # about this, just to help the author avoid writing invalid Transformer names.
256              
257 0 0         if (not @disabled_transformer_names) {
258 0           @disabled_transformer_names = @transformer_name_patterns;
259             }
260             }
261              
262 0           return hashify(@disabled_transformer_names);
263             }
264              
265             #-----------------------------------------------------------------------------
266              
267             1;
268              
269             __END__
270              
271             =pod
272              
273             =head1 NAME
274              
275             Perl::ToPerl6::Annotation - A "## no mogrify" annotation in a document.
276              
277              
278             =head1 SYNOPSIS
279              
280             use Perl::ToPerl6::Annotation;
281             $annotation = Perl::ToPerl6::Annotation->new( -element => $no_mogrify_ppi_element );
282              
283             $bool = $annotation->disables_line( $number );
284             $bool = $annotation->disables_transformer( $transformer_object );
285             $bool = $annotation->disables_all_transformers();
286              
287             ($start, $end) = $annotation->effective_range();
288             @disabled_transformer_names = $annotation->disabled_transformers();
289              
290              
291             =head1 DESCRIPTION
292              
293             C<Perl::ToPerl6::Annotation> represents a single C<"## no mogrify">
294             annotation in a L<PPI:Document>. The Annotation takes care of parsing
295             the annotation and keeps track of which lines and Transformers it affects.
296             It is intended to encapsulate the details of the no-mogrify
297             annotations, and to provide a way for Transformer objects to interact with
298             the annotations (via a L<Perl::ToPerl6::Document|Perl::ToPerl6::Document>).
299              
300              
301             =head1 INTERFACE SUPPORT
302              
303             This is considered to be a non-public class. Its interface is subject
304             to change without notice.
305              
306              
307             =head1 CLASS METHODS
308              
309             =over
310              
311             =item create_annotations( -doc => $doc )
312              
313             Given a L<Perl::ToPerl6::Document|Perl::ToPerl6::Document>, finds all the C<"## no mogrify">
314             annotations and constructs a new C<Perl::ToPerl6::Annotation> for each
315             one and returns them. The order of the returned objects is not
316             defined. It is generally expected that clients will use this
317             interface rather than calling the C<Perl::ToPerl6::Annotation>
318             constructor directly.
319              
320              
321             =back
322              
323              
324             =head1 CONSTRUCTOR
325              
326             =over
327              
328             =item C<< new( -element => $ppi_annotation_element ) >>
329              
330             Returns a reference to a new Annotation object. The B<-element>
331             argument is required and should be a C<PPI::Token::Comment> that
332             conforms to the C<"## no mogrify"> syntax.
333              
334              
335             =back
336              
337              
338             =head1 METHODS
339              
340             =over
341              
342             =item C<< disables_line( $line ) >>
343              
344             Returns true if this Annotation disables C<$line> for any (or all)
345             Transformers.
346              
347              
348             =item C<< disables_transformer( $transformer_object ) >>
349              
350             =item C<< disables_transformer( $transformer_name ) >>
351              
352             Returns true if this Annotation disables C<$polciy_object> or
353             C<$transformer_name> at any (or all) lines.
354              
355              
356             =item C<< disables_all_transformers() >>
357              
358             Returns true if this Annotation disables all Transformers at any (or all)
359             lines. If this method returns true, C<disabled_transformers> will return
360             an empty list.
361              
362              
363             =item C<< effective_range() >>
364              
365             Returns a two-element list, representing the first and last line
366             numbers where this Annotation has effect.
367              
368              
369             =item C<< disabled_transformers() >>
370              
371             Returns a list of the names of the Transformers that are affected by this
372             Annotation. If this list is empty, then it means that all Transformers
373             are affected by this Annotation, and C<disables_all_transformers()> should
374             return true.
375              
376              
377             =item C<< element() >>
378              
379             Returns the L<PPI::Element|PPI::Element> where this annotation started. This is
380             typically an instance of L<PPI::Token::Comment|PPI::Token::Comment>.
381              
382              
383             =back
384              
385              
386             =head1 AUTHOR
387              
388             Jeffrey Ryan Thalhammer <jeff@imaginative-software.com>
389              
390              
391             =head1 COPYRIGHT
392              
393             Copyright (c) 2005-2011 Imaginative Software Systems. All rights reserved.
394              
395             This program is free software; you can redistribute it and/or modify
396             it under the same terms as Perl itself. The full text of this license
397             can be found in the LICENSE file included with this module.
398              
399             =cut
400              
401             ##############################################################################
402             # Local Variables:
403             # mode: cperl
404             # cperl-indent-level: 4
405             # fill-column: 78
406             # indent-tabs-mode: nil
407             # c-indentation-style: bsd
408             # End:
409             # ex: set ts=8 sts=4 sw=4 tw=78 ft=perl expandtab shiftround :