File Coverage

blib/lib/PLS/Server/Request/TextDocument/PublishDiagnostics.pm
Criterion Covered Total %
statement 132 159 83.0
branch 15 42 35.7
condition 9 25 36.0
subroutine 27 28 96.4
pod 0 4 0.0
total 183 258 70.9


line stmt bran cond sub pod time code
1              
2             use strict;
3 9     9   54 use warnings;
  9         17  
  9         216  
4 9     9   36  
  9         18  
  9         190  
5             use parent 'PLS::Server::Request';
6 9     9   36  
  9         9  
  9         36  
7             use Encode;
8 9     9   512 use File::Basename;
  9         17  
  9         775  
9 9     9   54 use File::Path;
  9         97  
  9         463  
10 9     9   46 use File::Spec;
  9         25  
  9         378  
11 9     9   46 use File::Temp;
  9         34  
  9         223  
12 9     9   46 use IO::Async::Function;
  9         17  
  9         504  
13 9     9   47 use IO::Async::Loop;
  9         9  
  9         341  
14 9     9   44 use IO::Async::Process;
  9         58  
  9         252  
15 9     9   45 use Path::Tiny;
  9         18  
  9         144  
16 9     9   28 use Perl::Critic;
  9         18  
  9         404  
17 9     9   4085 use PPI;
  9         5006923  
  9         538  
18 9     9   75 use URI;
  9         18  
  9         145  
19 9     9   46  
  9         18  
  9         176  
20             use PLS::Parser::Pod;
21 9     9   45 use PLS::Server::State;
  9         18  
  9         187  
22 9     9   45  
  9         26  
  9         12578  
23             =head1 NAME
24              
25             PLS::Server::Request::TextDocument::PublishDiagnostics
26              
27             =head1 DESCRIPTION
28              
29             This is a message from the server to the client requesting that
30             diagnostics be published.
31              
32             These diagnostics currently include compilation errors and linting (using L<perlcritic>).
33              
34             =cut
35              
36             my $function = IO::Async::Function->new(code => \&run_perlcritic);
37              
38             my $loop = IO::Async::Loop->new();
39             $loop->add($function);
40              
41             {
42             my ($class, %args) = @_;
43              
44 2     2 0 29 return if (ref $PLS::Server::State::CONFIG ne 'HASH');
45              
46 2 50       19 my $uri = URI->new($args{uri});
47             return if (ref $uri ne 'URI::file');
48 2         34  
49 2 50       232 my $self = bless {
50             method => 'textDocument/publishDiagnostics',
51 2         27 params => {
52             uri => $uri->as_string,
53             diagnostics => []
54             },
55             notification => 1
56             },
57             $class;
58              
59             my (undef, $dir) = File::Basename::fileparse($uri->file);
60              
61 2         86 my $source = $uri->file;
62             my $text = PLS::Parser::Document->text_from_uri($uri->as_string);
63 2         523 $source = $text if (ref $text eq 'SCALAR');
64 2         182 my $version = PLS::Parser::Document::uri_version($uri->as_string);
65 2 50       16 my $client_has_version_support = $PLS::Server::State::CLIENT_CAPABILITIES->{textDocument}{publishDiagnostics}{versionSupport};
66 2         22 $self->{params}{version} = $version if (length $version and $client_has_version_support);
67 2         16  
68 2 50 33     110 # If closing, return empty list of diagnostics.
69             return Future->done($self) if $args{close};
70              
71 2 50       37 my @futures;
72              
73 2         12 push @futures, get_compilation_errors($source, $dir) if (defined $PLS::Server::State::CONFIG->{syntax}{enabled} and $PLS::Server::State::CONFIG->{syntax}{enabled});
74             push @futures, get_perlcritic_errors($source, $uri->file)
75 2 50 33     40 if (defined $PLS::Server::State::CONFIG->{perlcritic}{enabled} and $PLS::Server::State::CONFIG->{perlcritic}{enabled});
76              
77 2 50 33     182 return Future->wait_all(@futures)->then(
78             sub {
79             my $current_version = PLS::Parser::Document::uri_version($uri->as_string);
80              
81 1     1   231860 # No version will be returned if the document has been closed.
82             # Since the only way we got here is if the document is open, we
83             # should return nothing, since any diagnostics we return will be from
84             # when the document was still open.
85             return Future->done(undef) unless (length $current_version);
86              
87 1 50       11 # If the document has been updated since the diagnostics were created,
88             # send nothing back. The next update will re-trigger the diagnostics.
89             return Future->done(undef) if (length $version and $current_version > $version);
90              
91 1 50 33     20 @{$self->{params}{diagnostics}} = map { $_->result } @_;
92              
93 1         8 return Future->done($self);
  1         33  
  2         35  
94             }
95 1         12 );
96             } ## end sub new
97 2         7121  
98             {
99             my ($source, $dir) = @_;
100              
101             my $temp;
102 2     2 0 43 my $future = $loop->new_future();
103              
104 2         4 my $fh;
105 2         14 my $path;
106              
107 2         71 if (ref $source eq 'SCALAR')
108             {
109             $temp = eval { File::Temp->new(CLEANUP => 0, TEMPLATE => '.pls-tmp-XXXXXXXXXX', DIR => $dir) };
110 2 50       17 $temp = eval { File::Temp->new(CLEANUP => 0) } if (ref $temp ne 'File::Temp');
111             $path = $temp->filename;
112 2         11  
  2         93  
113 2 50       1841 $future->on_done(sub { unlink $temp });
  0         0  
114 2         30 my $source_text = Encode::encode_utf8($$source);
115              
116 2     1   63 print {$temp} $source_text;
  1         68  
117 2         104 close $temp;
118              
119 2         40 open $fh, '<', \$source_text;
  2         35  
120 2         115 } ## end if (ref $source eq 'SCALAR'...)
121             else
122 2     2   202 {
  2         32  
  2         8  
  2         61  
123             $path = $source;
124             open $fh, '<', $path or return [];
125             }
126 0         0  
127 0 0       0 my @line_lengths;
128              
129             while (my $line = <$fh>)
130 2         2869 {
131             chomp $line;
132 2         18 $line_lengths[$.] = length $line;
133             }
134 234         265  
135 234         470 close $fh;
136              
137             my $perl = PLS::Parser::Pod->get_perl_exe();
138 2         9 my $inc = PLS::Parser::Pod->get_clean_inc();
139             my $args = PLS::Parser::Pod->get_perl_args();
140 2         48 my @inc = map { "-I$_" } @{$inc // []};
141 2         28 my $index = PLS::Parser::Index->new();
142 2         57 my $workspace_folder = List::Util::first { path($_)->subsumes($path) } @{$index->workspace_folders};
143 2   50     14 ($workspace_folder) = @{$index->workspace_folders} unless (length $workspace_folder);
  16         95  
  2         35  
144 2         27 my $new_cwd = $PLS::Server::State::CONFIG->{cwd} // '';
145 2     2   61 $new_cwd =~ s/\$ROOT_PATH/$workspace_folder/;
  2         49  
  2         22  
146 2 50       681  
  0         0  
147 2   50     335 my @setup;
148 2         29 push @setup, (chdir => $new_cwd) if (length $new_cwd and -d $new_cwd);
149              
150 2         10 my @diagnostics;
151 2 50 33     24  
152             my $proc = IO::Async::Process->new(
153 2         12 command => [$perl, @inc, '-c', $path, @{$args}],
154             setup => \@setup,
155             stderr => {
156 2         176 on_read => sub {
157             my ($stream, $buffref, $eof) = @_;
158              
159             while ($$buffref =~ s/^(.*)\n//)
160 2     2   1207689 {
161             my $line = $1;
162 2         54 next if $line =~ /syntax OK$/;
163              
164 1         17 # Hide warnings from circular references
165 1 50       17 next if $line =~ /Subroutine .+ redefined/;
166              
167             # Hide "BEGIN failed" and "Compilation failed" messages - these provide no useful info.
168 0 0       0 #next if $line =~ /^BEGIN failed/;
169             #next if $line =~ /^Compilation failed/;
170             if (my ($error, $file, $line, $area) = $line =~ /^(.+) at (.+?) line (\d+)(, .+)?/)
171             {
172             $error .= $area if (length $area);
173 0 0       0 $line = int $line;
174             next if $file ne $path;
175 0 0       0  
176 0         0 push @diagnostics,
177 0 0       0 {
178             range => {
179 0         0 start => {line => $line - 1, character => 0},
180             end => {line => $line - 1, character => $line_lengths[$line]}
181             },
182             message => $error,
183             severity => 1,
184             source => 'perl',
185             };
186             } ## end if (my ($error, $file,...))
187              
188             } ## end while ($$buffref =~ s/^(.*)\n//...)
189              
190             return 0;
191             }
192             },
193 2         20 stdout => {
194             on_read => sub {
195             my ($stream, $buffref) = @_;
196              
197             # Discard STDOUT, otherwise it might interfere with the server execution.
198 1     1   730 # This can happen if there is a BEGIN block that prints to STDOUT.
199             $$buffref = '';
200             return 0;
201             }
202 1         9 },
203 1         6 on_finish => sub {
204             $future->done(@diagnostics);
205             }
206             );
207 1     1   1018  
208             $loop->add($proc);
209 2         13  
210             return $future;
211 2         2124 } ## end sub get_compilation_errors
212              
213 2         15567 {
214             my ($source, $path) = @_;
215              
216             my ($profile) = glob $PLS::Server::State::CONFIG->{perlcritic}{perlcriticrc};
217             undef $profile if (not length $profile or not -f $profile or not -r $profile);
218 2     2 0 786  
219             return $function->call(args => [$profile, $source, $path]);
220 2         201 } ## end sub get_perlcritic_errors
221 2 50 33     128  
      33        
222             {
223 2         137 my ($profile, $source, $path) = @_;
224              
225             my $critic = Perl::Critic->new(-profile => $profile);
226             my %args;
227             $args{filename} = $path if (ref $source eq 'SCALAR');
228 0     0 0 0 my $doc = PPI::Document->new($source, %args);
229             my @violations = eval { $critic->critique($doc) };
230 0         0  
231 0         0 my @diagnostics;
232 0 0       0  
233 0         0 # Mapping from perlcritic severity to LSP severity
234 0         0 my %severity_map = (
  0         0  
235             5 => 1,
236 0         0 4 => 1,
237             3 => 2,
238             2 => 3,
239 0         0 1 => 3
240             );
241              
242             foreach my $violation (@violations)
243             {
244             my $severity = $severity_map{$violation->severity};
245              
246             my $doc = URI->new();
247 0         0 $doc->scheme('https');
248             $doc->authority('metacpan.org');
249 0         0 $doc->path('pod/' . $violation->policy);
250              
251 0         0 push @diagnostics,
252 0         0 {
253 0         0 range => {
254 0         0 start => {line => $violation->line_number - 1, character => $violation->column_number - 1},
255             end => {line => $violation->line_number - 1, character => $violation->column_number + length($violation->source) - 1}
256 0         0 },
257             message => $violation->description,
258             code => $violation->policy,
259             codeDescription => {href => $doc->as_string},
260             severity => $severity,
261             source => 'perlcritic'
262             };
263             } ## end foreach my $violation (@violations...)
264              
265             return @diagnostics;
266             } ## end sub run_perlcritic
267              
268             1;