File Coverage

blib/lib/PLS/Parser/PackageSymbols.pm
Criterion Covered Total %
statement 67 74 90.5
branch 5 14 35.7
condition 3 7 42.8
subroutine 18 21 85.7
pod 0 6 0.0
total 93 122 76.2


line stmt bran cond sub pod time code
1             package PLS::Parser::PackageSymbols;
2              
3 13     13   101 use strict;
  13         29  
  13         569  
4 13     13   75 use warnings;
  13         31  
  13         1205  
5 13     13   90 use feature 'state';
  13         35  
  13         2069  
6              
7 13     13   111 use Future;
  13         19  
  13         625  
8 13     13   85 use IO::Async::Loop;
  13         25  
  13         573  
9 13     13   74 use IO::Async::Process;
  13         26  
  13         373  
10              
11 13     13   1020 use PLS::JSON;
  13         29  
  13         20335  
12              
13             =head1 NAME
14              
15             PLS::Parser::PackageSymbols
16              
17             =head1 DESCRIPTION
18              
19             This package executes a Perl process to import a package and interrogate
20             its symbol table to find all of the symbols in the package.
21              
22             =cut
23              
24             my $package_symbols_process;
25             my $imported_symbols_process;
26              
27             sub get_package_symbols
28             {
29 0     0 0 0 my ($config, @packages) = @_;
30              
31 0 0       0 return Future->done({}) unless (scalar @packages);
32              
33 0 0       0 start_package_symbols_process($config) if (ref $package_symbols_process ne 'IO::Async::Process');
34              
35 0         0 return _send_data_and_recv_result($package_symbols_process, \@packages);
36             } ## end sub get_package_symbols
37              
38             sub get_imported_package_symbols
39             {
40 2     2 0 17 my ($config, @imports) = @_;
41              
42 2 50       18 return Future->done({}) unless (scalar @imports);
43              
44 2 50       62 start_imported_package_symbols_process($config) if (ref $imported_symbols_process ne 'IO::Async::Process');
45              
46 2         21 return _send_data_and_recv_result($imported_symbols_process, \@imports);
47             } ## end sub get_imported_package_symbols
48              
49             sub _start_process
50             {
51 8     8   107 my ($config, $code) = @_;
52              
53 8         158 require PLS::Parser::Pod;
54 8         639 my $perl = PLS::Parser::Pod->get_perl_exe();
55 8         66 my @inc = map { "-I$_" } @{$config->{inc}};
  0         0  
  8         64  
56 8         117 my $args = PLS::Parser::Pod->get_perl_args();
57              
58 8         225 my $script_name = $0 =~ s/'/\'/gr;
59 8         115 $code = "\$0 = '$script_name';\n$code";
60              
61             my $process = IO::Async::Process->new(
62 8         223 command => [$perl, @inc, '-e', $code, @{$args}],
63             setup => _get_setup($config),
64             stdin => {via => 'pipe_write'},
65             stdout => {
66 1     1   347 on_read => sub { 0 }
67             },
68       0     on_finish => sub { }
69 8         25 );
70              
71 8         8606 IO::Async::Loop->new->add($process);
72              
73 8         156294 return $process;
74             } ## end sub _start_process
75              
76             sub _send_data_and_recv_result
77             {
78 2     2   18 my ($process, $data) = @_;
79              
80 2         58 $data = encode_json $data;
81              
82 2     2   7429 return $process->stdin->write("$data\n")->then(sub { $process->stdout->read_until("\n") })->then(
83             sub {
84 1     1   758390 my ($json) = @_;
85              
86 1   50     12 return Future->done(eval { decode_json $json } // {});
  1         35  
87             },
88 0     0   0 sub { Future->done({}) }
89 2         52 );
90             } ## end sub _send_data_and_recv_result
91              
92             sub start_package_symbols_process
93             {
94 4     4 0 122 my ($config) = @_;
95              
96 4 50       55 eval { $package_symbols_process->kill('TERM') } if (ref $package_symbols_process eq 'IO::Async::Process');
  0         0  
97 4         129 $package_symbols_process = _start_process($config, get_package_symbols_code());
98              
99 4         265 return;
100             } ## end sub start_package_symbols_process
101              
102             sub start_imported_package_symbols_process
103             {
104 4     4 0 196 my ($config) = @_;
105              
106 4 50       293 eval { $imported_symbols_process->kill('TERM') } if (ref $package_symbols_process eq 'IO::Async::Process');
  4         171  
107 4         411 $imported_symbols_process = _start_process($config, get_imported_package_symbols_code());
108              
109 4         307 return;
110             } ## end sub start_imported_package_symbols_process
111              
112             sub _get_setup
113             {
114 8     8   21 my ($config) = @_;
115              
116 8         58 require PLS::Parser::Index;
117              
118             # Just use the first workspace folder as ROOT_PATH - we don't know
119             # which folder the code will ultimately be in, and it doesn't really matter
120             # for anyone except me.
121 8         71 my ($workspace_folder) = @{PLS::Parser::Index->new->workspace_folders};
  8         292  
122 8   50     100 my $cwd = $config->{cwd} // '';
123 8         18 $cwd =~ s/\$ROOT_PATH/$workspace_folder/;
124 8         27 my @setup;
125 8 50 33     105 push @setup, (chdir => $cwd) if (length $cwd and -d $cwd);
126              
127 8         690 return \@setup;
128             } ## end sub _get_setup
129              
130             sub get_package_symbols_code
131             {
132 4     4 0 123 my $code = <<'EOF';
133             close STDERR;
134              
135             use B;
136              
137             my $json_package = 'JSON::PP';
138              
139             if (eval { require Cpanel::JSON::XS; 1 })
140             {
141             $json_package = 'Cpanel::JSON::XS';
142             }
143             elsif (eval { require JSON::XS; 1 })
144             {
145             $json_package = 'JSON::XS';
146             }
147             else
148             {
149             require JSON::PP;
150             }
151              
152             $| = 1;
153              
154             my $json = $json_package->new->utf8;
155              
156             package PackageSymbols;
157              
158             my %mtimes;
159              
160             while (my $line = <STDIN>)
161             {
162             my $packages_to_find = $json->decode($line);
163             my %functions;
164              
165             foreach my $find_package (@{$packages_to_find})
166             {
167             my @module_parts = split /::/, $find_package;
168             my @parent_module_parts = @module_parts;
169             pop @parent_module_parts;
170              
171             my @packages;
172              
173             foreach my $parts (\@parent_module_parts, \@module_parts)
174             {
175             my $package = join '::', @{$parts};
176             next unless (length $package);
177              
178             my $package_path = $package =~ s/::/\//gr;
179             $package_path .= '.pm';
180              
181             if (exists $mtimes{$package_path} and $mtimes{$package_path} != (stat $INC{$package_path})[9])
182             {
183             delete $INC{$package_path};
184             }
185              
186             eval "require $package";
187             next unless (length $INC{$package_path});
188              
189             $mtimes{$package_path} = (stat $INC{$package_path})[9];
190              
191             push @packages, $package;
192              
193             my @isa = add_parent_classes($package);
194              
195             foreach my $isa (@isa)
196             {
197             my $isa_path = $isa =~ s/::/\//gr;
198             $isa_path .= '.pm';
199              
200             if (exists $mtimes{$isa_path} and $mtimes{$isa_path} != (stat $INC{$isa_path})[9])
201             {
202             delete $INC{$isa_path};
203             }
204              
205             eval "require $isa";
206             next if (length $@);
207              
208             $mtimes{$isa_path} = (stat $INC{$isa_path})[9];
209              
210             push @packages, $isa;
211             } ## end foreach my $isa (@isa)
212             } ## end foreach my $parts (\@parent_module_parts...)
213              
214             foreach my $package (@packages)
215             {
216             my @parts = split /::/, $package;
217             my $ref = \%{"${package}::"};
218              
219             foreach my $name (keys %{$ref})
220             {
221             next if $name =~ /^BEGIN|UNITCHECK|INIT|CHECK|END|VERSION|DESTROY|import|unimport|can|isa$/;
222             next if $name =~ /^_/; # hide private subroutines
223             next if $name =~ /^\(/; # overloaded operators start with a parenthesis
224              
225             my $code_ref = $package->can($name);
226             next if (ref $code_ref ne 'CODE');
227             my $defined_in = eval { B::svref_2object($code_ref)->GV->STASH->NAME };
228             next if ($defined_in ne $package and not $package->isa($defined_in));
229              
230             if ($find_package->isa($package))
231             {
232             push @{$functions{$find_package}}, $name;
233             }
234             else
235             {
236             push @{$functions{$package}}, $name;
237             }
238             } ## end foreach my $name (keys %{$ref...})
239             } ## end foreach my $package (@packages...)
240             } ## end foreach my $find_package (@...)
241              
242             print $json->encode(\%functions);
243             print "\n";
244             } ## end while (my $line = <STDIN>...)
245              
246             sub add_parent_classes
247             {
248             my ($package) = @_;
249              
250             my @isa = eval "\@${package}::ISA";
251             return unless (scalar @isa);
252              
253             foreach my $isa (@isa)
254             {
255             push @isa, add_parent_classes($isa);
256             }
257              
258             return @isa;
259             } ## end sub add_parent_classes
260             EOF
261              
262 4         75 return $code;
263             } ## end sub get_package_symbols_code
264              
265             sub get_imported_package_symbols_code
266             {
267 4     4 0 319 my $code = <<'EOF';
268             #close STDERR;
269              
270             my $json_package = 'JSON::PP';
271              
272             if (eval { require Cpanel::JSON::XS; 1 })
273             {
274             $json_package = 'Cpanel::JSON::XS';
275             }
276             elsif (eval { require JSON::XS; 1 })
277             {
278             $json_package = 'JSON::XS';
279             }
280             else
281             {
282             require JSON::PP;
283             }
284              
285             $| = 1;
286              
287             my $json = $json_package->new->utf8;
288              
289             package ImportedPackageSymbols;
290              
291             my %mtimes;
292             my %symbol_cache;
293              
294             while (my $line = <STDIN>)
295             {
296             my $imports = $json->decode($line);
297              
298             my %functions;
299              
300             foreach my $import (@{$imports})
301             {
302             my $module_path = $import->{module} =~ s/::/\//gr;
303             $module_path .= '.pm';
304              
305             if (exists $mtimes{$module_path})
306             {
307             if ($mtimes{$module_path} == (stat $INC{$module_path})[9])
308             {
309             if (ref $symbol_cache{$module->{use}} eq 'ARRAY')
310             {
311             foreach my $subroutine (@{$symbol_cache{$module->{use}}})
312             {
313             $functions{$import->{module}}{$subroutine} = 1;
314             }
315              
316             next;
317             } ## end if (ref $symbol_cache{...})
318             } ## end if (length $module_abs_path...)
319             else
320             {
321             delete $INC{$module_path};
322             }
323             }
324              
325             my %symbol_table_before = %ImportedPackageSymbols::;
326             eval $import->{use};
327             my %symbol_table_after = %ImportedPackageSymbols::;
328             delete @symbol_table_after{keys %symbol_table_before};
329              
330             my @subroutines;
331              
332             foreach my $subroutine (keys %symbol_table_after)
333             {
334             # Constants are created as scalar refs in the symbol table
335             next if (ref $symbol_table_after{$subroutine} ne 'SCALAR' and ref $symbol_table_after{$subroutine} ne 'GLOB' and ref \($symbol_table_after{$subroutine}) ne 'GLOB');
336             next if ((ref $symbol_table_after{$subroutine} eq 'GLOB' or ref \($symbol_table_after{$subroutine}) eq 'GLOB') and ref *{$symbol_table_after{$subroutine}}{CODE} ne 'CODE');
337             $functions{$import->{module}}{$subroutine} = 1;
338             push @subroutines, $subroutine;
339             } ## end foreach my $subroutine (keys...)
340              
341             # Reset symbol table
342             %ImportedPackageSymbols:: = %symbol_table_before;
343              
344             $mtimes{$module_path} = (stat $INC{$module_path})[9];
345             $symbol_cache{$import->{use}} = \@subroutines;
346             } ## end foreach my $import (@{$imports...})
347              
348             foreach my $module (keys %functions)
349             {
350             $functions{$module} = [keys %{$functions{$module}}];
351             }
352              
353             print $json->encode(\%functions);
354             print "\n";
355             } ## end while (my $line = <STDIN>...)
356             EOF
357              
358 4         198 return $code;
359             } ## end sub get_imported_package_symbols_code
360              
361             1;