File Coverage

blib/lib/PLS/Parser/Element.pm
Criterion Covered Total %
statement 63 166 37.9
branch 16 90 17.7
condition 1 84 1.1
subroutine 18 31 58.0
pod 21 23 91.3
total 119 394 30.2


line stmt bran cond sub pod time code
1             package PLS::Parser::Element;
2              
3 11     11   77 use strict;
  11         23  
  11         309  
4 11     11   45 use warnings;
  11         21  
  11         325  
5              
6 11     11   61 use List::Util qw(any first);
  11         28  
  11         636  
7 11     11   64 use Scalar::Util qw(blessed);
  11         21  
  11         24310  
8              
9             =head1 NAME
10              
11             PLS::Parser::Element
12              
13             =head1 DESCRIPTION
14              
15             This is an abstraction of a L<PPI::Element> with additional functionality.
16              
17             =head1 METHODS
18              
19             =cut
20              
21             sub new
22             {
23 555     555 0 1415 my ($class, @args) = @_;
24              
25 555         961 my %args = @args;
26              
27 555         1119 my %self = (ppi_element => $args{element}, file => $args{file}, document => $args{document});
28 555 50 33     2095 return if (not blessed($args{element}) or not $args{element}->isa('PPI::Element'));
29 555         1670 return bless \%self, $class;
30             } ## end sub new
31              
32             =head2 ppi_line_number
33              
34             This is the line number of the element according to PPI.
35              
36             =cut
37              
38             sub ppi_line_number
39             {
40 162     162 1 182 my ($self) = @_;
41              
42 162         220 return $self->element->line_number;
43             }
44              
45             =head2 ppi_column_number
46              
47             This is the column number of the element according to PPI.
48              
49             =cut
50              
51             sub ppi_column_number
52             {
53 2157     2157 1 2512 my ($self) = @_;
54              
55 2157         2630 return $self->element->column_number;
56             }
57              
58             =head2 lsp_line_number
59              
60             This is the line number of the element according to the Language Server Protocol.
61              
62             =cut
63              
64             sub lsp_line_number
65             {
66 152     152 1 184 my ($self) = @_;
67              
68 152         215 my $line_number = $self->ppi_line_number;
69 152 50       1802 return 0 unless $line_number;
70 152         345 return $line_number - 1;
71             } ## end sub lsp_line_number
72              
73             =head2 lsp_column_number
74              
75             This is the column number of the element according to the Language Server Protocol.
76              
77             =cut
78              
79             sub lsp_column_number
80             {
81 2157     2157 1 2511 my ($self) = @_;
82              
83 2157         2487 my $column_number = $self->ppi_column_number;
84 2157 50       23751 return 0 unless $column_number;
85 2157         3942 return $column_number - 1;
86             } ## end sub lsp_column_number
87              
88             =head2 location_info
89              
90             This is information about the location of the element, to be stored in the index.
91              
92             =cut
93              
94             sub location_info
95             {
96 0     0 1 0 my ($self) = @_;
97              
98             return {
99             file => $self->{file},
100 0         0 location => {
101             line_number => $self->lsp_line_number,
102             column_number => $self->lsp_column_number
103             }
104             };
105             } ## end sub location_info
106              
107             =head2 content
108              
109             This is the content of the element.
110             This is the same as L<PPI::Element::content>.
111              
112             =cut
113              
114             sub content
115             {
116 369     369 1 444 my ($self) = @_;
117              
118 369         440 return $self->element->content;
119             }
120              
121             =head2 name
122              
123             This is the name of the element.
124             This is the same as the result of C<content>, in the base class.
125              
126             =cut
127              
128             sub name
129             {
130 97     97 1 140 my ($self) = @_;
131              
132 97         145 return $self->content;
133             }
134              
135             =head2 package_name
136              
137             This finds a package name at the given column number inside this element.
138              
139             =cut
140              
141             sub package_name
142             {
143 0     0 1 0 my ($self, $column_number) = @_;
144              
145 0         0 my $element = $self->element;
146 0         0 $column_number++;
147              
148 0 0 0     0 if ( blessed($element->statement)
      0        
149             and $element->statement->isa('PPI::Statement::Include')
150             and $element->statement->type eq 'use')
151             {
152             # This is a 'use parent/base' statement. The import is a package, not a subroutine.
153 0 0 0     0 if ($element->statement->module eq 'parent' or $element->statement->module eq 'base')
154             {
155 0         0 my $import = _extract_import($element, $column_number);
156 0 0       0 return $import if (length $import);
157             }
158              
159             # This is likely a 'use' statement with an explicit subroutine import.
160 0         0 my $package = $element->statement->module;
161 0         0 my $import = _extract_import($element, $column_number);
162 0 0       0 return $element->statement->module, $import if (length $import);
163             } ## end if (blessed($element->...))
164              
165             # Regular use statement, no explicit imports
166 0 0 0     0 if (blessed($element->statement) and $element->statement->isa('PPI::Statement::Include') and $element->statement->type eq 'use')
      0        
167             {
168 0         0 return $element->statement->module;
169             }
170              
171             # Class method call, cursor is over the package name
172 0 0 0     0 if ( $element->isa('PPI::Token::Word')
      0        
173             and ref $element->snext_sibling eq 'PPI::Token::Operator'
174             and $element->snext_sibling eq '->')
175             {
176 0         0 return $element->content;
177             } ## end if ($element->isa('PPI::Token::Word'...))
178              
179             # Declaring parent class using @ISA directly.
180 0 0 0     0 if ( blessed($element->statement)
      0        
      0        
181             and $element->statement->isa('PPI::Statement::Variable')
182             and $element->statement->type eq 'our'
183 0     0   0 and any { $_->symbol eq '@ISA' } $element->statement->symbols)
184             {
185 0         0 my $import = _extract_import($element, $column_number);
186 0 0       0 return $import if (length $import);
187             } ## end if (blessed($element->...))
188              
189 0         0 return;
190             } ## end sub package_name
191              
192             =head2 method_name
193              
194             This finds a method name in the current element.
195              
196             =cut
197              
198             sub method_name
199             {
200 0     0 1 0 my ($self) = @_;
201              
202 0         0 my $element = $self->element;
203              
204             return
205 0 0 0     0 if ( not blessed($element)
      0        
206             or not $element->isa('PPI::Token::Word')
207             or not blessed($element->sprevious_sibling)
208             or not $element->sprevious_sibling->isa('PPI::Token::Operator')
209             or $element->sprevious_sibling ne '->');
210              
211 0         0 return $element->content =~ s/^SUPER:://r;
212             } ## end sub method_name
213              
214             =head2 class_method_package_and_name
215              
216             This finds a class method within the current element and returns the class and method name.
217              
218             =cut
219              
220             sub class_method_package_and_name
221             {
222 0     0 1 0 my ($self) = @_;
223              
224 0         0 my $element = $self->element;
225              
226             return
227 0 0 0     0 if ( not blessed($element)
      0        
      0        
      0        
      0        
      0        
228             or not $element->isa('PPI::Token::Word')
229             or not blessed($element->sprevious_sibling)
230             or not $element->sprevious_sibling->isa('PPI::Token::Operator')
231             or not $element->sprevious_sibling eq '->'
232             or not blessed($element->sprevious_sibling->sprevious_sibling)
233             or not $element->sprevious_sibling->sprevious_sibling->isa('PPI::Token::Word'));
234              
235 0         0 return ($element->sprevious_sibling->sprevious_sibling->content, $element->content);
236             } ## end sub class_method_package_and_name
237              
238             =head2 subroutine_package_and_name
239              
240             This finds a fully qualified function call within this element and returns the package
241             and function name.
242              
243             =cut
244              
245             sub subroutine_package_and_name
246             {
247 0     0 1 0 my ($self) = @_;
248              
249 0         0 my $element = $self->element;
250              
251 0 0       0 return unless blessed($element);
252              
253 0         0 my $content = '';
254              
255 0 0 0     0 return if ( blessed($element->sprevious_sibling)
      0        
256             and $element->sprevious_sibling->isa('PPI::Token::Operator')
257             and $element->sprevious_sibling eq '->');
258              
259 0 0 0     0 if ($element->isa('PPI::Token::Symbol') and $element->content =~ /^&/)
    0          
260             {
261 0         0 $content = $element->content =~ s/^&//r;
262             }
263             elsif ($element->isa('PPI::Token::Word'))
264             {
265 0         0 $content = $element->content;
266             }
267             else
268             {
269 0         0 return;
270             }
271              
272 0 0       0 if ($content =~ /::/)
273             {
274 0         0 my @parts = split /::/, $content;
275 0         0 my $subroutine = pop @parts;
276 0         0 my $package = join '::', @parts;
277 0         0 return $package, $subroutine;
278             } ## end if ($content =~ /::/)
279             else
280             {
281 0         0 return '', $content;
282             }
283              
284 0         0 return;
285             } ## end sub subroutine_package_and_name
286              
287             =head2 variable_name
288              
289             This finds a variable in the current element and returns its name.
290              
291             =cut
292              
293             sub variable_name
294             {
295 0     0 1 0 my ($self) = @_;
296              
297 0         0 my $element = $self->element;
298 0 0 0     0 return if (not blessed($element) or not $element->isa('PPI::Token::Symbol'));
299              
300 0         0 return $element->symbol;
301             } ## end sub variable_name
302              
303             =head2 cursor_on_package
304              
305             This determines if the cursor at the given column number is on a package name.
306              
307             =cut
308              
309             sub cursor_on_package
310             {
311 0     0 1 0 my ($self, $column_number) = @_;
312              
313 0         0 my $element = $self->element;
314              
315 0         0 my $index = $column_number - $element->column_number;
316 0         0 my @parts = split /::/, $element->content;
317 0         0 my $current_index = 1;
318              
319 0         0 for (my $i = 0 ; $i <= $#parts ; $i++)
320             {
321 0         0 my $part = $parts[$i];
322              
323 0 0       0 if ($index <= $current_index + length $part)
324             {
325 0 0       0 return 0 if ($i == $#parts);
326 0         0 pop @parts;
327 0         0 return 1;
328             } ## end if ($index <= $current_index...)
329              
330 0         0 $current_index += length $part;
331             } ## end for (my $i = 0 ; $i <= ...)
332              
333 0         0 return;
334             } ## end sub cursor_on_package
335              
336             =head2 _extract_import
337              
338             This extracts an import within a C<use> statement, which may be a package or function name.
339              
340             =cut
341              
342             sub _extract_import
343             {
344 0     0   0 my ($element, $column_number) = @_;
345              
346             # Single import, single quotes or 'q' string.
347 0 0 0     0 if ($element->isa('PPI::Token::Quote::Single') or $element->isa('PPI::Token::Quote::Literal'))
348             {
349 0         0 return $element->literal;
350             }
351              
352             # Single import, double quotes or 'qq' string.
353 0 0 0     0 if ($element->isa('PPI::Token::Quote::Double') or $element->isa('PPI::Token::Quote::Interpolate'))
354             {
355 0         0 return $element->string;
356             }
357              
358             # Multiple imports, 'qw' list.
359 0 0       0 if ($element->isa('PPI::Token::QuoteLike::Words'))
360             {
361 0         0 my $import = _get_string_from_qw($element, $column_number);
362 0 0       0 return $import if (length $import);
363             }
364              
365             # Multiple imports, using a list.
366 0 0       0 if ($element->isa('PPI::Structure::List'))
367             {
368 0         0 my $import = _get_string_from_list($element, $column_number);
369 0 0       0 return $import if (length $import);
370             }
371              
372 0         0 return;
373             } ## end sub _extract_import
374              
375             =head2 _get_string_from_list
376              
377             This finds the string in a list at a given column number.
378              
379             =cut
380              
381             sub _get_string_from_list
382             {
383 0     0   0 my ($element, $column_number) = @_;
384              
385 0         0 foreach my $expr ($element->children)
386             {
387 0 0       0 next unless $expr->isa('PPI::Statement::Expression');
388              
389 0         0 foreach my $item ($expr->children)
390             {
391             # Only handle quoted strings. Could be another or list, but that's too complicated.
392 0 0       0 next unless $item->isa('PPI::Token::Quote');
393              
394 0 0 0     0 if ($item->column_number <= $column_number and ($item->column_number + length $item->content) >= $column_number)
395             {
396 0 0       0 return $item->literal if ($item->can('literal'));
397 0         0 return $item->string;
398             }
399             } ## end foreach my $item ($expr->children...)
400             } ## end foreach my $expr ($element->...)
401             } ## end sub _get_string_from_list
402              
403             =head2 _get_string_from_qw
404              
405             This gets a string from a C<qw> quoted list at a given column number.
406              
407             =cut
408              
409             sub _get_string_from_qw
410             {
411 0     0   0 my ($element, $column_number) = @_;
412              
413 0         0 my ($content) = $element->content =~ /qw[[:graph:]](.+)[[:graph:]]/;
414 0 0       0 return unless (length $content);
415 0         0 my @words = split /(\s+)/, $content;
416 0         0 my $current_column = $element->column_number + 3;
417              
418             # Figure out which word the mouse is hovering on.
419 0         0 foreach my $word (@words)
420             {
421 0         0 my $next_start = $current_column + length $word;
422              
423 0 0 0     0 if ($word !~ /^\s*$/ and $current_column <= $column_number and $next_start > $column_number)
      0        
424             {
425 0         0 return $word;
426             }
427              
428 0         0 $current_column = $next_start;
429             } ## end foreach my $word (@words)
430             } ## end sub _get_string_from_qw
431              
432             =head2 range
433              
434             This provides the range where this element is located, in a format the
435             Language Server Protocol can understand.
436              
437             =cut
438              
439             sub range
440             {
441 76     76 1 114 my ($self) = @_;
442              
443 76         107 my $lines = () = $self->element->content =~ m{($/)}g;
444 76         509 my ($last_line) = $self->element->content =~ m{(.+)$/$};
445 76 50       409 my $last_line_length = defined $last_line ? length $last_line : length $self->element->content;
446              
447             return {
448 76 50       275 start => {
449             line => $self->lsp_line_number,
450             character => $self->lsp_column_number
451             },
452             end => {
453             line => $self->lsp_line_number + $lines,
454             character => $lines == 0 ? $self->lsp_column_number + $last_line_length : $last_line_length
455             }
456             };
457             } ## end sub range
458              
459             =head2 length
460              
461             This returns the length of this element.
462              
463             =cut
464              
465             sub length
466             {
467 0     0 1 0 my ($self) = @_;
468              
469 0         0 return length $self->name;
470             }
471              
472             =head2 parent
473              
474             This returns the parent element of this element, as a L<PLS::Parser::Element> object.
475              
476             =cut
477              
478             sub parent
479             {
480 92     92 1 128 my ($self) = @_;
481              
482 92 50       153 return $self->{_parent} if (ref $self->{_parent} eq 'PLS::Parser::Element');
483 92 50       121 return unless $self->element->parent;
484 92         790 return PLS::Parser::Element->new(file => $self->{file}, element => $self->element->parent);
485             } ## end sub parent
486              
487             =head2 previous_sibling
488              
489             This returns the previous significant sibling of this element, as a L<PLS::Parser::Element> object.
490              
491             =cut
492              
493             sub previous_sibling
494             {
495 79     79 1 170 my ($self) = @_;
496              
497 79 100       246 return $self->{_previous_sibling} if (ref $self->{_previous_sibling} eq 'PLS::Parser::Element');
498 24 100       42 return unless $self->element->sprevious_sibling;
499 18         415 $self->{_previous_sibling} = PLS::Parser::Element->new(file => $self->{file}, element => $self->element->sprevious_sibling);
500 18         101 return $self->{_previous_sibling};
501             } ## end sub previous_sibling
502              
503             =head2 previous_sibling
504              
505             This returns the next significant sibling of this element, as a L<PLS::Parser::Element> object.
506              
507             =cut
508              
509             sub next_sibling
510             {
511 30     30 0 220 my ($self) = @_;
512              
513 30 100       90 return $self->{_next_sibling} if (ref $self->{_next_sibling} eq 'PLS::Parser::Element');
514 5 50       12 return unless $self->element->snext_sibling;
515 5         141 $self->{_next_sibling} = PLS::Parser::Element->new(file => $self->{file}, element => $self->element->snext_sibling);
516 5         28 return $self->{_next_sibling};
517             } ## end sub next_sibling
518              
519             =head2 children
520              
521             This returns all of this element's children, as L<PLS::Parser::Element> objects.
522              
523             =cut
524              
525             sub children
526             {
527 0     0 1 0 my ($self) = @_;
528              
529 0 0       0 return @{$self->{_children}} if (ref $self->{_children} eq 'ARRAY');
  0         0  
530 0 0       0 return unless $self->element->can('children');
531 0         0 $self->{_children} = [map { PLS::Parser::Element->new(file => $self->{file}, element => $_) } $self->element->children];
  0         0  
532 0         0 return @{$self->{_children}};
  0         0  
533             } ## end sub children
534              
535             =head2 tokens
536              
537             This returns all the tokens in the current element, as L<PLS::Parser::Element> objects.
538             Tokens correspond to all of the L<PPI::Token> objects in the current element.
539              
540             =cut
541              
542             sub tokens
543             {
544 157     157 1 222 my ($self) = @_;
545              
546 157 50       314 return @{$self->{_tokens}} if (ref $self->{_tokens} eq 'ARRAY');
  0         0  
547 157 50       242 return unless $self->element->can('tokens');
548 157         258 $self->{_tokens} = [map { PLS::Parser::Element->new(file => $self->{file}, element => $_) } $self->element->tokens];
  283         1620  
549 157         206 return @{$self->{_tokens}};
  157         417  
550             } ## end sub tokens
551              
552             =head2 element
553              
554             Returns the L<PPI::Element> object for this element.
555              
556             =cut
557              
558             sub element
559             {
560 5652     5652 1 6572 my ($self) = @_;
561              
562 5652         11981 return $self->{ppi_element};
563             }
564              
565             =head2 type
566              
567             Returns the type of L<PPI::Element> that this element is associated with.
568              
569             =cut
570              
571             sub type
572             {
573 1916     1916 1 2344 my ($self) = @_;
574              
575 1916         2216 return ref $self->element;
576             }
577              
578             1;