| 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; |