File Coverage

blib/lib/Module/Patch.pm
Criterion Covered Total %
statement 132 166 79.5
branch 58 110 52.7
condition 13 34 38.2
subroutine 13 14 92.8
pod 1 3 33.3
total 217 327 66.3


line stmt bran cond sub pod time code
1             package Module::Patch;
2              
3             our $DATE = '2019-01-06'; # DATE
4             our $VERSION = '0.275'; # VERSION
5              
6 1     1   2022 use 5.010001;
  1         3  
7 1     1   4 use strict 'subs', 'vars';
  1         2  
  1         27  
8 1     1   5 use warnings;
  1         2  
  1         21  
9 1     1   1257 use Log::ger;
  1         41  
  1         4  
10              
11 1     1   601 use Monkey::Patch::Action qw();
  1         3051  
  1         19  
12 1     1   354 use Package::Stash;
  1         6800  
  1         36  
13 1     1   371 use Package::Util::Lite qw(package_exists);
  1         364  
  1         591  
14              
15             our @EXPORT_OK = qw(patch_package);
16              
17             sub is_loaded {
18 6     6 0 11 my $mod = shift;
19              
20 6         18 (my $mod_pm = "$mod.pm") =~ s!::!/!g;
21 6 100       27 exists($INC{$mod_pm}) && $INC{$mod_pm};
22             }
23              
24             my%loaded_by_us;
25              
26             sub import {
27 6     6   10158 my $self = shift;
28              
29 6         17 my $caller = caller;
30              
31 6 50       180 if ($self eq __PACKAGE__) {
32             # we are not subclassed, provide exports
33 0         0 for my $exp (@_) {
34             die "$exp is not exported by ".__PACKAGE__
35 0 0       0 unless grep { $_ eq $exp } @EXPORT_OK;
  0         0  
36 0         0 *{"$caller\::$exp"} = \&{$_};
  0         0  
  0         0  
37             }
38             } else {
39             # we are subclassed, patch caller with patch_data()
40 6         14 my %opts = @_;
41              
42 6         7 my $load;
43 6 50       16 if (exists $opts{-load_target}) {
44 0         0 $load = $opts{-load_target};
45 0         0 delete $opts{-load_target};
46             }
47 6   50     26 $load //= 1;
48 6         8 my $force;
49 6 100       37 if (exists $opts{-force}) {
50 1         3 $force = $opts{-force};
51 1         2 delete $opts{-force};
52             }
53 6   100     19 $force //= 0;
54 6         7 my $warn;
55 6 50       13 if (exists $opts{-warn_target_loaded}) {
56 0         0 $warn = $opts{-warn_target_loaded};
57 0         0 delete $opts{-warn_target_loaded};
58             }
59 6   50     18 $warn //= 1;
60              
61             # patch already applied, ignore
62 6 50       6 return if ${"$self\::handles"};
  6         27  
63              
64 6 100       8 unless (${"$self\::patch_data_cached"}) {
  6         25  
65 3         10 ${"$self\::patch_data_cached"} = $self->patch_data;
  3         34  
66             }
67              
68 6 50       6 my $pdata = ${"$self\::patch_data_cached"} or
  6         17  
69             die "BUG: $self: No patch data supplied";
70 6   50     21 my $v = $pdata->{v} // 1;
71 6         7 my $curv = 3;
72 6 50 33     21 if ($v == 1 || $v == 2) {
    50          
73 0         0 my $mpv;
74 0 0       0 if ($v == 1) {
    0          
75 0         0 $mpv = "0.06 or earlier";
76             } elsif ($v == 2) {
77 0         0 $mpv = "0.07-0.09";
78             }
79 0   0     0 die "$self ".( ${"$self\::VERSION" } // "?" ).
  0   0     0  
80             " requires Module::Patch $mpv (patch_data format v=$v),".
81             " this is Module::Patch ".($Module::Patch::VERSION // '?').
82             " (v=$curv), please install an older version of ".
83             "Module::Patch or upgrade $self";
84             } elsif ($v == 3) {
85             # ok, current version
86             } else {
87 0         0 die "BUG: $self: Unknown patch_data format version ($v), ".
88             "only v=$curv supported by this version of Module::Patch";
89             }
90              
91 6         8 my $target = $self;
92 6 50       39 $target =~ s/(?<=\w)::[Pp]atch::\w+$//
93             or die "BUG: $self: Bad patch module name '$target', it should ".
94             "end with '::Patch::YourCategory'";
95              
96 6 100       13 if (is_loaded($target)) {
97 5 50       13 if (!$loaded_by_us{$target}) {
98 0 0 0     0 if ($load && $warn) {
99 0         0 warn "$target is loaded before ".__PACKAGE__.", this is ".
100             "not recommended since $target might export subs ".
101             "before " . __PACKAGE__." gets the chance to patch " .
102             "them";
103             }
104             }
105             } else {
106 1 50       3 if ($load) {
107 1     1   346 eval "package $caller; use $target";
  1         29  
  1         16  
  1         54  
108 1 50       5 die if $@;
109 1         2 $loaded_by_us{$target}++;
110             } else {
111 0 0       0 if ($warn) {
112 0         0 warn "$target does not exist and we are told not to load ".
113             "it, skipped patching";
114             }
115 0         0 return;
116             }
117             }
118              
119             # read patch module's configs
120 1     1   7 no warnings 'once';
  1         1  
  1         1093  
121 6   100     19 my $pcdata = $pdata->{config} // {};
122 6         8 my $config = \%{"$self\::config"};
  6         16  
123 6         22 while (my ($k, $v) = each %$pcdata) {
124 6 50       18 die "Invalid configuration defined by $self\::patch_data(): ".
125             "$k: must start with dash" unless $k =~ /\A-/;
126 6         10 $config->{$k} = $v->{default};
127 6 100       17 if (exists $opts{$k}) {
128 1         3 $config->{$k} = $opts{$k};
129 1         3 delete $opts{$k};
130             }
131             }
132              
133 6 100       16 if (keys %opts) {
134 1         12 die "$self: Unknown option(s): ".join(", ", keys %opts);
135             }
136              
137 5 50       8 if ($pdata->{after_read_config}) {
138 0         0 $pdata->{after_read_config}->();
139             }
140              
141 5 50       10 if ($pdata->{before_patch}) {
142 0         0 $pdata->{before_patch}->();
143             }
144              
145 5         21 log_trace "Module::Patch: patching $target with $self ...";
146 4         13 ${"$self\::handles"} = patch_package(
147             $target, $pdata->{patches},
148 5   33     29 {force=>$force, patch_module=>ref($self) || $self});
149              
150 4 50       24 if ($pdata->{after_patch}) {
151 0         0 $pdata->{after_patch}->();
152             }
153              
154             }
155             }
156              
157             sub unimport {
158 4     4   7733 my $self = shift;
159              
160 4 50       19 if ($self eq __PACKAGE__) {
161             # we are not subclassed, do nothing
162             } else {
163 4 50       6 my $pdata = ${"$self\::patch_data_cached"} or
  4         18  
164             die "BUG: $self: No patch data supplied";
165              
166 4 50       8 if ($pdata->{before_unpatch}) {
167 0         0 $pdata->{before_unpatch}->();
168             }
169              
170 4         5 my $handles = ${"$self\::handles"};
  4         10  
171 4         17 log_trace "Module::Patch: Unpatching $self ...";
172 4         9 undef ${"$self\::handles"};
  4         9  
173             # do we need to undef ${"$self\::config"}?, i'm thinking not really
174              
175 4 50       17 if ($pdata->{after_unpatch}) {
176 0         0 $pdata->{after_unpatch}->();
177             }
178              
179             }
180             }
181              
182             sub patch_data {
183 0     0 0 0 die "BUG: patch_data() should be provided by subclass";
184             }
185              
186             sub patch_package {
187 5     5 1 11 my ($package0, $patches_spec, $opts) = @_;
188 5   50     12 $opts //= {};
189              
190 5         8 my $handles = {};
191 5 50       13 for my $target (ref($package0) eq 'ARRAY' ? @$package0 : ($package0)) {
192 5 50       13 die "FATAL: Target module '$target' not loaded"
193             unless package_exists($target);
194 5         61 my $target_version = ${"$target\::VERSION"};
  5         11  
195 5         6 my $target_subs;
196              
197 5         5 my $i = 0;
198             PATCH:
199 5         10 for my $pspec (@$patches_spec) {
200 7         11 my $act = $pspec->{action};
201 7 50       20 my $errp = ($opts->{patch_module} ? "$opts->{patch_module}: ":"").
202             "patch[$i]"; # error prefix
203 7 50       14 $act or die "BUG: $errp: no action supplied";
204 7 50       25 $act =~ /\A(wrap|add|replace|add_or_replace|delete)\z/ or die
205             "BUG: $errp: action '$pspec->{action}' unknown";
206 7 50       14 if ($act eq 'delete') {
207 0 0       0 $pspec->{code} and die "BUG: $errp: for action 'delete', ".
208             "code must not be supplied";
209             } else {
210 7 50       11 $pspec->{code} or die "BUG: $errp: code not supplied";
211             }
212              
213             my $sub_names = ref($pspec->{sub_name}) eq 'ARRAY' ?
214 7 50       17 [@{ $pspec->{sub_name} }] : [$pspec->{sub_name}];
  0         0  
215 7         12 for (@$sub_names) {
216 7 50       325 $_ = qr/.*/ if $_ eq ':all';
217 7 50       12 $_ = qr/^_/ if $_ eq ':private';
218 7 50       10 $_ = qr/^[^_]/ if $_ eq ':public';
219 7 50       15 die "BUG: $errp: unknown tag in sub_name $_" if /^:/;
220             }
221              
222 7         7 my @s;
223 7         10 for my $sub_name (@$sub_names) {
224 7 50       11 if (ref($sub_name) eq 'Regexp') {
225 0 0       0 unless ($target_subs) {
226 0         0 $target_subs = [Package::Stash->new($target)->list_all_symbols("CODE")];
227             }
228 0         0 for (@$target_subs) {
229 0 0 0     0 push @s, $_ if $_ !~~ @s && $_ =~ $sub_name;
230             }
231             } else {
232 7         24 push @s, $sub_name;
233             }
234             }
235              
236 7 100 66     35 unless (!defined($pspec->{mod_version}) ||
237             $pspec->{mod_version} eq ':all') {
238 5 50 33     18 defined($target_version) && length($target_version)
239             or die "FATAL: Target package '$target' does not have ".
240             "\$VERSION";
241 5         6 my $mod_versions = $pspec->{mod_version};
242 5 100       13 $mod_versions = ref($mod_versions) eq 'ARRAY' ?
243             [@$mod_versions] : [$mod_versions];
244 5         9 for (@$mod_versions) {
245 7 50       11 $_ = qr/.*/ if $_ eq ':all';
246 7 50       13 die "BUG: $errp: unknown tag in mod_version $_"
247             if /^:/;
248             }
249              
250 5 100       11 unless (grep {
251 7 50       22 ref($_) eq 'Regexp' ? $target_version =~ $_ : $target_version eq $_
252             } @$mod_versions) {
253             warn "$errp: Target module version $target_version ".
254             "does not match [".join(", ", @$mod_versions)."], ".
255             ($opts->{force} ?
256 2 100       58 "patching anyway (force)":"skipped"). ".";
257 2 100       15 next PATCH unless $opts->{force};
258             }
259             }
260              
261 6         12 for my $s (@s) {
262             #log_trace("Patching %s ...", $s);
263             $handles->{"$target\::$s"} =
264             Monkey::Patch::Action::patch_package(
265 6         23 $target, $s, $act, $pspec->{code});
266             }
267              
268 5         405 $i++;
269             } # for $pspec
270             } # for $target
271 4         6 $handles;
272             }
273              
274             1;
275             # ABSTRACT: Patch package with a set of patches
276              
277             __END__