File Coverage

lib/Devel/Deanonymize.pm
Criterion Covered Total %
statement 19 44 43.1
branch 2 16 12.5
condition n/a
subroutine 8 9 88.8
pod 0 3 0.0
total 29 72 40.2


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Devel::Deanonymize - A small tool to make anonymous sub visible
4              
5             =head1 DESCRIPTION
6              
7             When collecting Coverage statistics with L a construct like below appear to be invisible and is simply ignored
8             by the statistic:
9              
10             my $sub = sub {
11             print "hello";
12             }
13              
14             This script aims to solve this problem by wrapping each file in a sub and thus making these subs I.
15             Code is based on L
16              
17             =head1 SYNOPSIS
18              
19             # Perl scripts
20             perl -MDevel::Cover=-ignore,^t/,Deanonymize -MDevel::Deanonymize= your_script.pl
21              
22             # Perl tests
23             HARNESS_PERL_SWITCHES="-MDevel::Cover=-ignore,^t/,Deanonymize -MDevel::Deanonymize=" prove t/
24              
25              
26             =head1 DEBUGGING
27              
28             If your tests suddenly fail for some weird reason, you can set C. If this environment variable is set,
29             we print out the filename for every modified file write its contents to C.
30              
31             It is also important to note that the regex which matches the end-marker is not perfect. In general it can be summarized
32             as follows:
33              
34             We start at the end of a file and search for the first occurrence of either C<__END__>, C<__DATA__> or C<1;>. To improve
35             robustness, these markers must occur alone on their respective line.
36             A special case is C<1> without semicolon: We only consider this case if its the very last character of a file.
37              
38             Files with no endmarkers at all are dangerous to use in conjunction with this module...
39              
40              
41             =head1 EXAMPLES
42              
43             Please referer to the files provided in the I directory
44              
45              
46             =head1 AUTHORS
47              
48             Since there is a lot of spam flooding my mailbox, I had to put spam filtering in place. If you want to make sure
49             that your email gets delivered into my mailbox, include C<#im_not_a_bot#> in the B
50              
51             Stobib at cpan.orgE>
52              
53             =head1 COPYRIGHT AND LICENSE
54              
55             MIT License
56              
57             Copyright (c) 2021 Tobias Bossert, OETIKER+PARTNER AG Switzerland
58              
59             Permission is hereby granted, free of charge, to any person obtaining a copy
60             of this software and associated documentation files (the "Software"), to deal
61             in the Software without restriction, including without limitation the rights
62             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
63             copies of the Software, and to permit persons to whom the Software is
64             furnished to do so, subject to the following conditions:
65              
66             The above copyright notice and this permission notice shall be included in all
67             copies or substantial portions of the Software.
68              
69             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
70             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
71             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
72             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
73             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
74             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
75             SOFTWARE.
76              
77             =cut
78              
79             package Devel::Deanonymize;
80 1     1   105888 use strict;
  1         2  
  1         27  
81 1     1   13 use warnings FATAL => 'all';
  1         2  
  1         26  
82 1     1   4 use base 'Exporter';
  1         2  
  1         786  
83              
84             our @EXPORT = qw(alterContent);
85              
86             our $VERSION = "0.2.0"; # Do not change manually, changed automatically on `make build` target
87              
88             my $include_pattern;
89              
90             sub import {
91             # capture input parameters
92 1 50   1   2183 $include_pattern = $_[1] ? $_[1] : die("Devel::Deanonymize: An include Pattern must be specified \n");
93             }
94              
95             sub alterContent {
96 6     6 0 5438 my $input = shift;
97 6         11 my $subName = shift;
98             # define everything in a sub, so Devel::Cover will DTRT
99             # NB this introduces no extra linefeeds so D::C's line numbers
100             # in reports match the file on disk
101             # - In general, we match only if *ENDMARKER*
102             # - We only allow `1` without a semicolon if found at the very end
103 6         111 $input =~ s/(.*?package\s+\S+)(.*)^[\s]*(__END__|1;|1\Z|__DATA__)[\s]*$(.*)\Z/$1sub $subName {$2} $subName();$3$4/sgm;
104              
105             # unhide private methods to avoid "Variable will not stay shared"
106             # warnings that appear due to change of applicable scoping rules
107             # Note: not '\s*' in the start of string, to avoid matching and
108             # removing blank lines before the private sub definitions.
109 6         35 $input =~ s/(^[\t| ]*)my\s+(\S+\s*=\s*sub.*)$/$1our $2/gm;
110              
111 6         17 return $input
112             }
113              
114             sub hasEndmarker {
115 0     0 0 0 my $input = shift;
116 0 0       0 if ($input =~ /^[\s]*(__END__|1;|1\Z|__DATA__)[\s]*$(.*)\Z/gms) {
117 0         0 return 1;
118             }
119 0         0 return 0;
120             }
121              
122              
123             sub modify_files {
124             # Internal notes:
125             # Basically, this code replaces every file path in @INC with a reference to an anonymous sub which wraps each
126             # file in sub classWrapper { $orig_content } classWrapper(); However, this sub is **not** necessarily run at INIT or UNITCHECK stage!
127             # NB, this also explains why its is possible to have $include_pattern "defined" at UNITCHECK even if its run **before** import()
128             # Also make sure each file either ends with __DATA__, __END__, or 1;
129             unshift @INC, sub {
130 6     6   72119 my (undef, $filename) = @_;
131 6 50       4790 return () if ($filename !~ /$include_pattern/);
132 0 0       0 if (my $found = (grep {-e $_} map {"$_/$filename"} grep {!ref} @INC)[0]) {
  0         0  
  0         0  
  0         0  
133 0 0       0 print "Devel::Deanonymize: $found" . "\n" if $ENV{DEANONYMIZE_DEBUG};
134 0         0 local $/ = undef;
135 0 0       0 open my $fh, '<', $found or die("Can't read module file $found\n");
136 0         0 my $module_text = <$fh>;
137 0         0 close $fh;
138              
139 0 0       0 if (not hasEndmarker($module_text)) {
140 0         0 warn("Devel::Deanonymize: Found no endmarker in file `$filename` - skipping\n");
141 0         0 return ();
142             }
143              
144 0         0 $module_text = alterContent($module_text, "_anon");
145              
146             # filehandle on the scalar
147 0         0 open $fh, '<', \$module_text;
148              
149 0 0       0 if ($ENV{DEANONYMIZE_DEBUG}) {
150 0         0 open my $mod_fh, '>', $found . "_mod.pl";
151 0         0 print $mod_fh $module_text;
152 0         0 close $mod_fh;
153             }
154              
155             # and put it into %INC too so that it looks like we loaded the code
156             # from the file directly
157 0         0 $INC{$filename} = $found;
158 0         0 return $fh;
159             }
160             else {
161 0         0 return ();
162             }
163 2     2 0 15 };
164             }
165              
166              
167             # We call modify_files twice since depending on how a module is loaded (use or required) it is present in @INC at different stages
168             # Also, "double-modification" is not possible because we only alter non references
169             INIT {
170 1     1   79 modify_files();
171             }
172              
173             UNITCHECK {
174             modify_files();
175             }
176              
177             1;