File Coverage

blib/lib/Perl/LanguageServer/Parser.pm
Criterion Covered Total %
statement 113 377 29.9
branch 0 128 0.0
condition 0 43 0.0
subroutine 38 43 88.3
pod 0 2 0.0
total 151 593 25.4


line stmt bran cond sub pod time code
1             package Perl::LanguageServer::Parser ;
2              
3 1     1   803 use Moose::Role ;
  1         2  
  1         9  
4              
5 1     1   5791 use Coro ;
  1         2  
  1         77  
6 1     1   8 use Coro::AIO ;
  1         3  
  1         339  
7 1     1   9 use JSON ;
  1         2  
  1         9  
8 1     1   161 use File::Basename ;
  1         4  
  1         79  
9              
10 1     1   18 use v5.16;
  1         3  
11              
12 1     1   8 no if $] >= 5.018, warnings => 'experimental'; # given, when, Smartmatch
  1         3  
  1         10  
13 1     1   95 no warnings 'uninitialized' ;
  1         13  
  1         42  
14 1     1   13 use feature 'switch'; # perl 5.16
  1         6  
  1         131  
15              
16 1     1   592 use Compiler::Lexer;
  1         6958  
  1         50  
17 1     1   8 use Data::Dump qw{dump} ;
  1         1  
  1         47  
18              
19 1     1   6 use constant SymbolKindFile => 1;
  1         2  
  1         80  
20 1     1   7 use constant SymbolKindModule => 2;
  1         2  
  1         52  
21 1     1   18 use constant SymbolKindNamespace => 3;
  1         2  
  1         55  
22 1     1   8 use constant SymbolKindPackage => 4;
  1         2  
  1         45  
23 1     1   7 use constant SymbolKindClass => 5;
  1         2  
  1         55  
24 1     1   7 use constant SymbolKindMethod => 6;
  1         2  
  1         54  
25 1     1   9 use constant SymbolKindProperty => 7;
  1         2  
  1         42  
26 1     1   6 use constant SymbolKindField => 8;
  1         6  
  1         56  
27 1     1   7 use constant SymbolKindConstructor => 9;
  1         1  
  1         51  
28 1     1   7 use constant SymbolKindEnum => 10;
  1         2  
  1         43  
29 1     1   5 use constant SymbolKindInterface => 11;
  1         2  
  1         103  
30 1     1   8 use constant SymbolKindFunction => 12;
  1         2  
  1         57  
31 1     1   16 use constant SymbolKindVariable => 13;
  1         2  
  1         57  
32 1     1   32 use constant SymbolKindConstant => 14;
  1         3  
  1         47  
33 1     1   9 use constant SymbolKindString => 15;
  1         2  
  1         81  
34 1     1   9 use constant SymbolKindNumber => 16;
  1         2  
  1         61  
35 1     1   7 use constant SymbolKindBoolean => 17;
  1         11  
  1         40  
36 1     1   11 use constant SymbolKindArray => 18;
  1         2  
  1         52  
37 1     1   10 use constant SymbolKindObject => 19;
  1         8  
  1         57  
38 1     1   13 use constant SymbolKindKey => 20;
  1         1  
  1         53  
39 1     1   12 use constant SymbolKindNull => 21;
  1         3  
  1         49  
40 1     1   6 use constant SymbolKindEnumMember => 22;
  1         2  
  1         50  
41 1     1   6 use constant SymbolKindStruct => 23;
  1         3  
  1         45  
42 1     1   5 use constant SymbolKindEvent => 24;
  1         2  
  1         58  
43 1     1   6 use constant SymbolKindOperator => 25;
  1         2  
  1         66  
44 1     1   6 use constant SymbolKindTypeParameter => 26;
  1         2  
  1         68  
45              
46 1     1   7 use constant CacheVersion => 5 ;
  1         2  
  1         4546  
47              
48              
49             # ---------------------------------------------------------------------------
50              
51             sub _get_docu
52             {
53 0     0     my ($self, $source, $line) = @_ ;
54              
55 0           my @docu ;
56             my $in_pod ;
57 0           while ($line-- >= 0)
58             {
59 0           my $src = $source -> [$line] ;
60 0 0         if ($src =~ /^=cut/)
61             {
62 0           $in_pod = 1 ;
63 0           next ;
64             }
65              
66 0 0         if ($in_pod)
67             {
68 0 0         last if ($src =~ /^=pod/) ;
69 0 0         next if ($src =~ /^=\w+\s*$/) ;
70 0           $src =~ s/^=item /* / ;
71 0           unshift @docu, $src ;
72             }
73             else
74             {
75 0 0         next if ($src =~ /^\s*$/) ;
76 0 0         next if ($src =~ /^\s*#[-#+~= \t]+$/) ;
77 0 0         last if ($src !~ /^\s*#(.*?)\s*$/) ;
78 0           unshift @docu, $1 ;
79             }
80             }
81              
82 0   0       shift @docu while (@docu && ($docu[0] =~ /^\s*$/)) ;
83 0   0       pop @docu while (@docu && ($docu[-1] =~ /^\s*$/)) ;
84              
85 0           return join ("\n", @docu) ;
86             }
87              
88              
89             # ---------------------------------------------------------------------------
90              
91              
92             sub parse_perl_source
93             {
94 0     0 0   my ($self, $uri, $source, $server) = @_ ;
95              
96 0           $source =~ s/\r//g ; # Compiler::Lexer computes wrong line numbers with \r
97 0           my @source = split /\n/, $source ;
98              
99 0           my $lexer = Compiler::Lexer->new();
100 0           my $tokens = $lexer->tokenize($source);
101              
102 0           cede () ;
103              
104             #$server -> logger (dump ($tokens) . "\n") ;
105              
106             #my $modules = $lexer->get_used_modules($script);
107              
108 0           my @vars ;
109 0           my $package = 'main::' ;
110 0           my %state ;
111             my $decl ;
112 0           my $declline ;
113 0           my $func ;
114 0           my $parent ;
115 0           my $top ;
116 0           my $add ;
117 0           my $func_param ;
118 0           my $token_ndx = -1 ;
119 0           my $brace_level = 0 ;
120 0           my @stack ;
121 0           my $beginchar = 0 ;
122 0           my $endchar = 0 ;
123              
124 0           foreach my $token (@$tokens)
125             {
126 0           $token_ndx++ ;
127 0           $token -> {data} =~ s/\r$// ;
128 0 0         $server -> logger ("token=", dump ($token), "\n") if ($Perl::LanguageServer::debug3) ;
129              
130 0 0 0       if (exists $state{method_mod} && $token -> {name} eq 'RawString')
131             {
132 0           $token -> {name} = 'Function' ;
133 0           delete $state{method_mod} ;
134             }
135              
136 0           given ($token -> {name})
137             {
138             when (['VarDecl', 'OurDecl', 'FunctionDecl'])
139 0           {
140             $decl = $token -> {data},
141 0           $declline = $token -> {line} ;
142             }
143             when (/Var$/)
144 0           {
145 0 0 0       $top = $decl eq 'our' || !$parent?\@vars:$parent ;
146             push @$top,
147             {
148             name => $token -> {data},
149 0 0         kind => SymbolKindVariable,
    0          
    0          
150             containerName => $decl eq 'our'?$package:$func,
151             ($decl?(definition => $decl):()),
152             ($decl eq 'my'?(localvar => $decl):()),
153             } ;
154 0           $add = $top -> [-1] ;
155 0 0         $token -> {line} = $declline if ($decl) ;
156 0           $decl = undef ;
157             }
158             when ('LeftBrace')
159 0           {
160 0           $brace_level++ ;
161 0           $decl = undef ;
162 0 0 0       if (@vars && $vars[-1]{kind} == SymbolKindVariable)
163             {
164 0           $vars[-1]{name} =~ s/^\$/%/ ;
165             }
166             }
167             when (['RightBrace', 'SemiColon'])
168 0           {
169 0 0         $brace_level-- if ($token -> {name} eq 'RightBrace') ;
170 0 0 0       if (@stack > 0 && $brace_level == $stack[-1]{brace_level})
171             {
172 0           my $stacktop = pop @stack ;
173 0           $parent = $stacktop -> {parent} ;
174 0           $func = $stacktop -> {func} ;
175 0           my $symbol = $stacktop -> {symbol} ;
176 0   0       my $start_line = $symbol -> {range}{start}{line} // $symbol -> {line} ;
177 0 0         $symbol -> {range} = { start => { line => $start_line, character => 0 }, end => { line => $token -> {line}-1, character => 9999 }}
178             if (defined ($start_line)) ;
179             }
180 0 0         if ($token -> {name} eq 'SemiColon')
181             {
182 0           $decl = undef ;
183 0           continue ;
184             }
185             }
186             when ('LeftBracket')
187 0           {
188 0 0 0       if (@vars && $vars[-1]{kind} == SymbolKindVariable)
189             {
190 0           $vars[-1]{name} =~ s/^\$/@/ ;
191             }
192             }
193             when (['Function', 'Method'])
194 0           {
195 0 0         if ($token -> {data} =~ /^\w/)
196             {
197 0 0         $top = !$parent?\@vars:$parent ;
198             push @$top,
199             {
200             name => $token -> {data},
201 0 0         kind => SymbolKindFunction,
    0          
202             containerName => @stack?$func:$package,
203             ($decl?(definition => $decl):()),
204             } ;
205 0           $func_param = $add = $top -> [-1] ;
206 0 0         if ($decl)
207             {
208 0           push @stack,
209             {
210             brace_level => $brace_level,
211             parent => $parent,
212             func => $func,
213             'package' => $package,
214             symbol => $add,
215             } ;
216 0           $token -> {line} = $declline ;
217 0           $func = $token -> {data} ;
218 0   0       $parent = $top -> [-1]{children} ||= [] ;
219             }
220 0           my $src = $source[$token -> {line}-1] ;
221 0           my $i ;
222 0 0 0       if ($src && ($i = index($src, $func) >= 0))
223             {
224 0           $beginchar = $i + 1 ;
225 0           $endchar = $i + 1 + length ($func) ;
226             }
227             }
228 0           $decl = undef ;
229             }
230             when ('ArgumentArray')
231 0           {
232 0 0         if ($func_param)
233             {
234 0           my @params ;
235 0 0 0       if ($tokens -> [$token_ndx - 1]{name} eq 'Assign' &&
236             $tokens -> [$token_ndx - 2]{name} eq 'RightParenthesis')
237             {
238 0           for (my $i = $token_ndx - 3; $i >= 0; $i--)
239             {
240 0 0         next if ($tokens -> [$i]{name} eq 'Comma') ;
241 0 0         last if ($tokens -> [$i]{name} !~ /Var$/) ;
242 0           push @params, $tokens -> [$i]{data} ;
243             }
244 0   0       my $func_doc = $self -> _get_docu (\@source, $func_param -> {range}{start}{line} // $func_param -> {line}) ;
245 0           my @parameters ;
246 0           foreach my $p (reverse @params)
247             {
248 0           push @parameters,
249             {
250             label => $p,
251             } ;
252             }
253 0           $func_param -> {detail} = '(' . join (',', reverse @params) . ')' ;
254             $func_param -> {signature} =
255             {
256             label => $func_param -> {name} . $func_param -> {detail},
257 0           documentation => $func_doc,
258             parameters => \@parameters
259             } ;
260             }
261 0           $func_param = undef ;
262             }
263             }
264             when ('Prototype')
265 0           {
266 0 0         if ($func_param)
267             {
268 0           my @params = split /\s*,\s*/, $token -> {data} ;
269 0   0       my $func_doc = $self -> _get_docu (\@source, $func_param -> {range}{start}{line} // $func_param -> {line}) ;
270 0           my @parameters ;
271 0           foreach my $p (@params)
272             {
273 0           push @parameters,
274             {
275             label => $p,
276             } ;
277             }
278 0           $func_param -> {detail} = '(' . join (',', @params) . ')' ;
279             $func_param -> {signature} =
280             {
281             label => $func_param -> {name} . $func_param -> {detail},
282 0           documentation => $func_doc,
283             parameters => \@parameters
284             } ;
285 0           $func_param = undef ;
286             }
287             }
288             when (['Package', 'UseDecl'] )
289 0           {
290 0           $state{is} = $token -> {data} ;
291 0           $state{module} = 1 ;
292             }
293             when (['ShortHashDereference', 'ShortArrayDereference'])
294 0           {
295 0           $state{scalar} = '$' ;
296             }
297             when ('Key')
298 0           {
299 0 0         if (exists ($state{constant}))
    0          
    0          
    0          
    0          
300             {
301 0           $top = \@vars ;
302             push @$top,
303             {
304             name => $token -> {data},
305 0           kind => SymbolKindConstant,
306             containerName => $package,
307             definition => 1,
308             } ;
309 0           $add = $top -> [-1] ;
310             }
311             elsif (exists ($state{scalar}))
312             {
313 0 0 0       $top = $decl eq 'our' || !$parent?\@vars:$parent ;
314             push @$top,
315             {
316             name => $state{scalar} . $token -> {data},
317 0 0         kind => SymbolKindVariable,
318             containerName => $decl eq 'our'?$package:$func,
319             } ;
320 0           $add = $top -> [-1] ;
321             }
322             elsif ($token -> {data} ~~ ['has', 'class_has'])
323             {
324 0           $state{has} = 1 ;
325             }
326             elsif ($token -> {data} ~~ ['around', 'before', 'after'])
327             {
328 0           $state{method_mod} = 1 ;
329             $decl = $token -> {data},
330 0           $declline = $token -> {line} ;
331             }
332             elsif ($token -> {data} =~ /^[a-z_][a-z0-9_]+$/i)
333             {
334 0           $top = \@vars ;
335             push @$top,
336             {
337             name => $token -> {data},
338 0           kind => SymbolKindFunction,
339             } ;
340 0           $add = $top -> [-1] ;
341             }
342             }
343             when ('RawString')
344 0           {
345 0 0         if (exists ($state{has}))
346             {
347 0           $top = \@vars ;
348             push @$top,
349             {
350             name => $token -> {data},
351 0           kind => SymbolKindProperty,
352             containerName => $package,
353             definition => 1,
354             } ;
355 0           $add = $top -> [-1] ;
356             }
357             }
358             when ('UsedName')
359 0           {
360 0 0         if ($token -> {data} eq 'constant')
361             {
362 0           delete $state{module} ;
363 0           $state{constant} = 1 ;
364             }
365             else
366             {
367 0           $state{ns} = [$token->{data}] ;
368             }
369             }
370             when ('Namespace')
371 0           {
372 0   0       $state{ns} ||= [] ;
373 0           push @{$state{ns}}, $token -> {data} ;
  0            
374             }
375             when ('NamespaceResolver')
376 0           {
377             # make sure it is not matched below
378             }
379             when ('Assign')
380 0           {
381 0           $decl = undef ;
382 0           continue ;
383             }
384             when ($token -> {data} =~ /^\W/)
385 0           {
386 0 0         if (exists ($state{ns}))
387             {
388 0 0         if ($state{module})
389             {
390 0           my $def ;
391 0 0         if ($state{is} eq 'package')
392             {
393 0           $def = 1 ;
394 0           $package = join ('::', @{$state{ns}}) ;
  0            
395 0           $top = \@vars ;
396 0           push @$top,
397             {
398             name => $package,
399             kind => SymbolKindModule,
400             #containerName => join ('::', @{$state{ns}}),
401             #($def?(definition => $def):()),
402             definition => 1,
403             } ;
404 0           $add = $top -> [-1] ;
405             }
406             else
407             {
408 0           my $name = pop @{$state{ns}} ;
  0            
409 0           $top = \@vars ;
410             push @$top,
411             {
412             name => $name,
413             kind => SymbolKindModule,
414 0 0         containerName => join ('::', @{$state{ns}}),
  0            
415             ($def?(definition => $def):()),
416             } ;
417 0           $add = $top -> [-1] ;
418             }
419             }
420             else
421             {
422 0           my $name = shift @{$state{ns}} ;
  0            
423 0           $top = \@vars ;
424             push @$top,
425             {
426             name => $name,
427             kind => SymbolKindFunction,
428 0           containerName => join ('::', @{$state{ns}}),
  0            
429             } ;
430 0           $add = $top -> [-1] ;
431             }
432             }
433              
434 0           %state = () ;
435             }
436             }
437 0 0         if ($add)
438             {
439 0 0         if (!$uri)
440             {
441 0           $add -> {line} = $token -> {line}-1 ;
442             }
443             else
444             {
445             #$add -> {location} = { uri => $uri, range => { start => { line => $token -> {line}-1, character => 0 }, end => { line => $token -> {line}-1, character => 0 }}} ;
446             $add -> {range} = { start => { line => $token -> {line}-1, character => 0 },
447 0 0         end => { line => $token -> {line}-1, character => ($endchar?9999:0) }} ;
448             $add -> {selectionRange} = { start => { line => $token -> {line}-1, character => $beginchar },
449 0           end => { line => $token -> {line}-1, character => $endchar }} ;
450 0           $beginchar = $endchar = 0 ;
451             }
452 0 0         $server -> logger ("var=", dump ($add), "\n") if ($Perl::LanguageServer::debug3) ;
453 0           $add = undef ;
454             }
455             }
456              
457 0 0         $server -> logger (dump (\@vars), "\n") if ($Perl::LanguageServer::debug3) ;
458              
459 0 0         return wantarray?(\@vars, $tokens):\@vars ;
460             }
461              
462              
463             # ----------------------------------------------------------------------------
464              
465             sub _parse_perl_source_cached
466             {
467 0     0     my ($self, $uri, $source, $path, $stats, $server) = @_ ;
468              
469 0           my $cachepath ;
470 0 0         if (!$self -> disable_cache)
471             {
472 0           my $escpath = $path ;
473 0           $escpath =~ s/:/%3A/ ;
474 0           $cachepath = $self -> state_dir .'/' . $escpath ;
475 0           $self -> mkpath (dirname ($cachepath)) ;
476              
477             #$server -> logger ("$path -> cachepath=$cachepath\n") ;
478 0           aio_stat ($cachepath) ;
479 0 0         if (-e _)
480             {
481 0           my $mtime_cache = -M _ ;
482 0           aio_stat ($path) ;
483 0           my $mtime_src = -M _ ;
484             #$server -> logger ("cache = $mtime_cache src = $mtime_src\n") ;
485 0 0         if ($mtime_src > $mtime_cache)
486             {
487             #$server -> logger ("load from cache\n") ;
488 0           my $cache ;
489 0           aio_load ($cachepath, $cache) ;
490 0           my $cache_data = eval { $Perl::LanguageServer::json -> decode ($cache) ; } ;
  0            
491 0 0         if ($@)
    0          
492             {
493 0           $self -> logger ("Loading of $cachepath failed, reparse file ($@)\n") ;
494             }
495             elsif (ref ($cache_data) eq 'HASH')
496             {
497 0 0         if ($cache_data -> {version} == CacheVersion)
498             {
499 0           $stats -> {loaded}++ ;
500 0           return $cache_data -> {vars} ;
501             }
502             }
503             }
504             }
505             }
506              
507 0           my $vars = $self -> parse_perl_source ($uri, $source, $server) ;
508              
509 0 0         if ($cachepath)
510             {
511 0 0         my $ifh = aio_open ($cachepath, IO::AIO::O_WRONLY | IO::AIO::O_TRUNC | IO::AIO::O_CREAT, 0664) or die "open $cachepath failed ($!)" ;
512 0           aio_write ($ifh, undef, undef, $Perl::LanguageServer::json -> encode ({ version => CacheVersion, vars => $vars}), 0) ;
513 0           aio_close ($ifh) ;
514             }
515              
516 0           $stats -> {parsed}++ ;
517              
518 0           return $vars ;
519             }
520              
521              
522              
523             # ----------------------------------------------------------------------------
524              
525             sub _parse_dir
526             {
527 0     0     my ($self, $server, $dir, $vars, $stats) = @_ ;
528              
529 0           my $text ;
530             my $fn ;
531 0           my $uri ;
532 0           my $file_vars ;
533              
534 0           my $filefilter = $self -> file_filter_regex ;
535 0           my $ignore_dir = $self -> ignore_dir ;
536              
537 0           my ($dirs, $files) = aio_scandir ($dir, 4) ;
538              
539 0 0         if ($dirs)
540             {
541 0           foreach my $d (sort @$dirs)
542             {
543 0 0         next if (exists $ignore_dir -> {$d}) ;
544 0           $self -> _parse_dir ($server, $dir . '/' . $d, $vars, $stats) ;
545             }
546             }
547              
548 0 0         if ($files)
549             {
550 0           foreach my $f (sort @$files)
551             {
552 0 0         next if ($f !~ /$filefilter/) ;
553              
554 0           $fn = $dir . '/' . $f ;
555 0           aio_load ($fn, $text) ;
556              
557 0           $uri = $self -> uri_server2client ('file://' . $fn) ;
558             #$server -> logger ("parse $fn -> $uri\n") ;
559 0           $file_vars = $self -> _parse_perl_source_cached (undef, $text, $fn, $stats, $server) ;
560 0           $vars -> {$uri} = $file_vars ;
561             #$server -> logger ("done $fn\n") ;
562 0           my $cnt = keys %$vars ;
563 0 0         $server -> logger ("loaded $stats->{loaded} files, parsed $stats->{parsed} files, $cnt files\n") if ($cnt % 100 == 0) ;
564             }
565             }
566              
567              
568             }
569              
570             # ----------------------------------------------------------------------------
571              
572             sub background_parser
573             {
574 0     0 0   my ($self, $server) = @_ ;
575              
576 0           my $channel = $self -> parser_channel ;
577 0           $channel -> shutdown ; # end other parser
578 0           cede ;
579              
580 0           $channel = $self -> parser_channel (Coro::Channel -> new) ;
581 0           my $folders = $self -> folders ;
582 0           $server -> logger ("background_parser folders = ", dump ($folders), "\n") ;
583 0           %{$self -> symbols} = () ;
  0            
584              
585 0           my $stats = {} ;
586 0           foreach my $dir (values %$folders)
587             {
588 0           $self -> _parse_dir ($server, $dir, $self -> symbols, $stats) ;
589 0           cede ;
590             }
591              
592 0           my $cnt = keys %{$self -> symbols} ;
  0            
593 0           $server -> logger ("initial parsing done, loaded $stats->{loaded} files, parsed $stats->{parsed} files, $cnt files\n") ;
594              
595 0           my $filefilter = $self -> file_filter_regex ;
596              
597 0           while (my $item = $channel -> get)
598             {
599 0           my ($cmd, $uri) = @$item ;
600              
601 0           my $fn = substr ($self -> uri_client2server ($uri), 7) ;
602 0 0         next if (basename ($fn) !~ /$filefilter/) ;
603              
604 0           my $text ;
605 0           aio_load ($fn, $text) ;
606              
607 0           $server -> logger ("parse $fn -> $uri\n") ;
608 0           my $file_vars = $self -> _parse_perl_source_cached (undef, $text, $fn, {}, $server) ;
609 0           $self -> symbols -> {$uri} = $file_vars ;
610             }
611              
612 0           $server -> logger ("background_parser quit\n") ;
613             }
614              
615              
616              
617             1 ;
618              
619              
620