File Coverage

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


line stmt bran cond sub pod time code
1             package PLS::Parser::Element;
2              
3 13     13   105 use strict;
  13         28  
  13         599  
4 13     13   126 use warnings;
  13         31  
  13         1223  
5              
6 13     13   1174 use List::Util qw(any first);
  13         28  
  13         1243  
7 13     13   91 use Scalar::Util qw(blessed);
  13         27  
  13         50605  
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 2103 my ($class, @args) = @_;
24              
25 555         1261 my %args = @args;
26              
27 555         1544 my %self = (ppi_element => $args{element}, file => $args{file}, document => $args{document});
28 555 50 33     2519 return if (not blessed($args{element}) or not $args{element}->isa('PPI::Element'));
29 555         2250 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 228 my ($self) = @_;
41              
42 162         300 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 3154 my ($self) = @_;
54              
55 2157         3466 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 264 my ($self) = @_;
67              
68 152         324 my $line_number = $self->ppi_line_number;
69 152 50       2214 return 0 unless $line_number;
70 152         390 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 3465 my ($self) = @_;
82              
83 2157         3456 my $column_number = $self->ppi_column_number;
84 2157 50       30903 return 0 unless $column_number;
85 2157         5504 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 607 my ($self) = @_;
117              
118 369         681 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 173 my ($self) = @_;
131              
132 97         216 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) ## no critic (RequireInterpolationOfMetachars)
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 foreach my $i (0 .. $#parts)
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 foreach my $i (0 .. $#parts...)
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              
402 0         0 return '';
403             } ## end sub _get_string_from_list
404              
405             =head2 _get_string_from_qw
406              
407             This gets a string from a C<qw> quoted list at a given column number.
408              
409             =cut
410              
411             sub _get_string_from_qw
412             {
413 0     0   0 my ($element, $column_number) = @_;
414              
415 0         0 my ($content) = $element->content =~ /qw[[:graph:]](.+)[[:graph:]]/;
416 0 0       0 return unless (length $content);
417 0         0 my @words = split /(\s+)/, $content;
418 0         0 my $current_column = $element->column_number + 3;
419              
420             # Figure out which word the mouse is hovering on.
421 0         0 foreach my $word (@words)
422             {
423 0         0 my $next_start = $current_column + length $word;
424              
425 0 0 0     0 if ($word !~ /^\s*$/ and $current_column <= $column_number and $next_start > $column_number)
      0        
426             {
427 0         0 return $word;
428             }
429              
430 0         0 $current_column = $next_start;
431             } ## end foreach my $word (@words)
432              
433 0         0 return '';
434             } ## end sub _get_string_from_qw
435              
436             =head2 range
437              
438             This provides the range where this element is located, in a format the
439             Language Server Protocol can understand.
440              
441             =cut
442              
443             sub range
444             {
445 76     76 1 147 my ($self) = @_;
446              
447 76         170 my $lines = () = $self->element->content =~ m{($/)}g;
448 76         748 my ($last_line) = $self->element->content =~ m{(.+)$/$};
449 76 50       509 my $last_line_length = defined $last_line ? length $last_line : length $self->element->content;
450              
451             return {
452 76 50       308 start => {
453             line => $self->lsp_line_number,
454             character => $self->lsp_column_number
455             },
456             end => {
457             line => $self->lsp_line_number + $lines,
458             character => $lines == 0 ? $self->lsp_column_number + $last_line_length : $last_line_length
459             }
460             };
461             } ## end sub range
462              
463             =head2 length
464              
465             This returns the length of this element.
466              
467             =cut
468              
469             sub length
470             {
471 0     0 1 0 my ($self) = @_;
472              
473 0         0 return length $self->name;
474             }
475              
476             =head2 parent
477              
478             This returns the parent element of this element, as a L<PLS::Parser::Element> object.
479              
480             =cut
481              
482             sub parent
483             {
484 92     92 1 176 my ($self) = @_;
485              
486 92 50       265 return $self->{_parent} if (ref $self->{_parent} eq 'PLS::Parser::Element');
487 92 50       181 return unless $self->element->parent;
488 92         718 return PLS::Parser::Element->new(file => $self->{file}, element => $self->element->parent);
489             } ## end sub parent
490              
491             =head2 previous_sibling
492              
493             This returns the previous significant sibling of this element, as a L<PLS::Parser::Element> object.
494              
495             =cut
496              
497             sub previous_sibling
498             {
499 79     79 1 245 my ($self) = @_;
500              
501 79 100       371 return $self->{_previous_sibling} if (ref $self->{_previous_sibling} eq 'PLS::Parser::Element');
502 24 100       55 return unless $self->element->sprevious_sibling;
503 18         668 $self->{_previous_sibling} = PLS::Parser::Element->new(file => $self->{file}, element => $self->element->sprevious_sibling);
504 18         109 return $self->{_previous_sibling};
505             } ## end sub previous_sibling
506              
507             =head2 previous_sibling
508              
509             This returns the next significant sibling of this element, as a L<PLS::Parser::Element> object.
510              
511             =cut
512              
513             sub next_sibling
514             {
515 30     30 0 131 my ($self) = @_;
516              
517 30 100       131 return $self->{_next_sibling} if (ref $self->{_next_sibling} eq 'PLS::Parser::Element');
518 5 50       13 return unless $self->element->snext_sibling;
519 5         217 $self->{_next_sibling} = PLS::Parser::Element->new(file => $self->{file}, element => $self->element->snext_sibling);
520 5         28 return $self->{_next_sibling};
521             } ## end sub next_sibling
522              
523             =head2 children
524              
525             This returns all of this element's children, as L<PLS::Parser::Element> objects.
526              
527             =cut
528              
529             sub children
530             {
531 0     0 1 0 my ($self) = @_;
532              
533 0 0       0 return @{$self->{_children}} if (ref $self->{_children} eq 'ARRAY');
  0         0  
534 0 0       0 return unless $self->element->can('children');
535 0         0 $self->{_children} = [map { PLS::Parser::Element->new(file => $self->{file}, element => $_) } $self->element->children];
  0         0  
536 0         0 return @{$self->{_children}};
  0         0  
537             } ## end sub children
538              
539             =head2 tokens
540              
541             This returns all the tokens in the current element, as L<PLS::Parser::Element> objects.
542             Tokens correspond to all of the L<PPI::Token> objects in the current element.
543              
544             =cut
545              
546             sub tokens
547             {
548 157     157 1 240 my ($self) = @_;
549              
550 157 50       456 return @{$self->{_tokens}} if (ref $self->{_tokens} eq 'ARRAY');
  0         0  
551 157 50       313 return unless $self->element->can('tokens');
552 157         291 $self->{_tokens} = [map { PLS::Parser::Element->new(file => $self->{file}, element => $_) } $self->element->tokens];
  283         2198  
553 157         260 return @{$self->{_tokens}};
  157         582  
554             } ## end sub tokens
555              
556             =head2 element
557              
558             Returns the L<PPI::Element> object for this element.
559              
560             =cut
561              
562             sub element
563             {
564 5652     5652 1 8506 my ($self) = @_;
565              
566 5652         17983 return $self->{ppi_element};
567             }
568              
569             =head2 type
570              
571             Returns the type of L<PPI::Element> that this element is associated with.
572              
573             =cut
574              
575             sub type
576             {
577 1916     1916 1 3044 my ($self) = @_;
578              
579 1916         3041 return ref $self->element;
580             }
581              
582             1;