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