File Coverage

blib/lib/PLS/Parser/PackageSymbols.pm
Criterion Covered Total %
statement 55 76 72.3
branch 3 14 21.4
condition 2 9 22.2
subroutine 15 25 60.0
pod 0 6 0.0
total 75 130 57.6


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