File Coverage

blib/lib/PLS/Parser/PackageSymbols.pm
Criterion Covered Total %
statement 71 78 91.0
branch 5 14 35.7
condition 3 7 42.8
subroutine 20 23 86.9
pod 0 6 0.0
total 99 128 77.3


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