File Coverage

blib/lib/Module/Extract/Namespaces.pm
Criterion Covered Total %
statement 92 100 92.0
branch 17 24 70.8
condition 2 2 100.0
subroutine 25 25 100.0
pod 11 11 100.0
total 147 162 90.7


line stmt bran cond sub pod time code
1             package Module::Extract::Namespaces;
2 3     3   2029 use strict;
  3         7  
  3         74  
3              
4 3     3   13 use warnings;
  3         5  
  3         69  
5 3     3   11 no warnings;
  3         5  
  3         100  
6              
7 3     3   1429 use subs qw();
  3         64  
  3         70  
8 3     3   14 use vars qw($VERSION);
  3         5  
  3         147  
9              
10             $VERSION = '1.021';
11              
12 3     3   14 use Carp qw(croak);
  3         6  
  3         134  
13 3     3   846 use File::Spec::Functions qw(splitdir catfile);
  3         1275  
  3         161  
14 3     3   1393 use PPI 1.126;
  3         376242  
  3         3017  
15              
16             =encoding utf8
17              
18             =head1 NAME
19              
20             Module::Extract::Namespaces - extract the package declarations from a module
21              
22             =head1 SYNOPSIS
23              
24             use Module::Extract::Namespaces;
25              
26             # in scalar context, extract first package namespace
27             my $namespace = Module::Extract::Namespaces->from_file( $filename );
28             if( Module::Extract::Namespaces->error ) { ... }
29              
30             # in list context, extract all namespaces
31             my @namespaces = Module::Extract::Namespaces->from_file( $filename );
32             if( Module::Extract::Namespaces->error ) { ... }
33              
34             # can do the Perl 5.12 package syntax with possible versions
35             # in list context, extract all namespaces and versions as duples
36             my @namespaces = Module::Extract::Namespaces->from_file( $filename, 1 );
37             if( Module::Extract::Namespaces->error ) { ... }
38              
39              
40             =head1 DESCRIPTION
41              
42             This module extracts package declarations from Perl code without
43             running the code.
44              
45             It does not extract:
46              
47             =over 4
48              
49             =item * packages declared dynamically (e.g. in C)
50              
51             =item * packages created as part of a fully qualified variable name
52              
53             =back
54              
55             =head2 Class methods
56              
57             =over 4
58              
59             =item from_module( MODULE, [ @DIRS ] )
60              
61             Extract the namespaces declared in MODULE. In list context, it returns
62             all of the namespaces, including possible duplicates. In scalar
63             context it returns the first declared namespace.
64              
65             You can specify a list of directories to search for the module. If you
66             don't, it uses C<@INC> by default.
67              
68             If it cannot find MODULE, it returns undef in scalar context and the
69             empty list in list context.
70              
71             On failure it returns nothing, but you have to check with C to
72             see if that is really an error or a file with no namespaces in it.
73              
74             =cut
75              
76             sub from_module {
77 2     2 1 16345 my( $class, $module, @dirs ) = @_;
78              
79 2 50       13 @dirs = @INC unless @dirs;
80 2         10 $class->_clear_error;
81              
82 2         9 my $absolute_path = $class->_module_to_file( $module, @dirs );
83 2 50       10 unless( defined $absolute_path ) {
84 0         0 $class->_set_error( "Did not find module [$module] in [@dirs]!" );
85 0         0 return;
86             }
87              
88 2 100       9 if( wantarray ) { my @a = $class->from_file( $absolute_path ) }
  1         8  
89 1         5 else { scalar $class->from_file( $absolute_path ) }
90             }
91              
92             sub _module_to_file {
93 2     2   9 my( $class, $module, @dirs ) = @_;
94              
95 2         22 my @module_parts = split /\b(?:::|')\b/, $module;
96 2         8 $module_parts[-1] .= '.pm';
97            
98 2         29 foreach my $dir ( @dirs ) {
99 2 50       52 unless( -d $dir ) {
100 0         0 carp( "The path [$dir] does not appear to be a directory" );
101 0         0 next;
102             }
103 2         35 my @dir_parts = splitdir( $dir );
104 2         41 my $path = catfile( @dir_parts, @module_parts );
105 2 50       44 next unless -e $path;
106 2         15 return $path;
107             }
108              
109 0         0 return;
110             }
111              
112             =item from_file( FILENAME [,WITH_VERSIONS] )
113              
114             Extract the namespaces declared in FILENAME. In list context, it
115             returns all of the namespaces, including possible duplicates. In
116             scalar context it returns the first declared namespace.
117              
118             If FILENAME does not exist, it returns undef in scalar context and the
119             empty list in list context.
120              
121             On failure it returns nothing, but you have to check with C to
122             see if that is really an error or a file with no namespaces in it.
123              
124             =cut
125              
126             sub from_file {
127 11     11 1 7990 my( $class, $file, $with_versions ) = @_;
128              
129 11         35 $class->_clear_error;
130              
131 11 100       177 unless( -e $file ) {
132 1         9 $class->_set_error( "File [$file] does not exist!" );
133 1         4 return;
134             }
135              
136 10         48 my $Document = $class->get_pdom( $file );
137 10 50       46 return unless $Document;
138              
139 10 100       34 my $method = $with_versions ?
140             'get_namespaces_and_versions_from_pdom'
141             :
142             'get_namespaces_from_pdom'
143             ;
144              
145 10         48 my @namespaces = $class->$method( $Document );
146              
147 10 100       28 if( wantarray ) { @namespaces }
  8         33  
148 2         21 else { $namespaces[0] }
149             }
150              
151              
152             =back
153              
154             =head2 Subclassable hooks
155              
156             =over 4
157              
158             =item $class->pdom_base_class()
159              
160             Return the base class for the PDOM. This is C by default. If you
161             want to use something else, you'll have to change all the other PDOM
162             methods to adapt to the different interface.
163              
164             This is the class name to use with C to load the module that
165             while handle the parsing.
166              
167             =cut
168              
169 10     10 1 23 sub pdom_base_class { 'PPI' }
170              
171             =item $class->pdom_document_class()
172              
173             Return the class name to use to create the PDOM object. This is
174             C.
175              
176             =cut
177              
178              
179 10     10 1 24 sub pdom_document_class { 'PPI::Document' }
180              
181             =item get_pdom( FILENAME )
182              
183             Creates the PDOM from FILENAME. This depends on calls to
184             C and C.
185              
186             =cut
187              
188             sub get_pdom {
189 10     10 1 27 my( $class, $file ) = @_;
190              
191 10         31 my $pdom_class = $class->pdom_base_class;
192              
193 10         640 eval "require $pdom_class";
194              
195 10         37 my $Document = eval {
196 10         32 my $pdom_document_class = $class->pdom_document_class;
197              
198 10         64 my $d = $pdom_document_class->new( $file );
199 10 50       224135 die $pdom_document_class->errstr unless $d;
200              
201 10         74 $class->pdom_preprocess( $d );
202 10         24 $d;
203             };
204              
205 10 50       45 if( $@ ) {
206 0         0 $class->_set_error( "Could not get PDOM for $file: $@" );
207 0         0 return;
208             }
209              
210 10         28 $Document;
211             }
212              
213             =item $class->pdom_preprocess( PDOM )
214              
215             Override this method to play with the PDOM before extracting the
216             package declarations.
217              
218             By default, it strips Pod and comments from the PDOM.
219              
220             =cut
221              
222             sub pdom_preprocess {
223 10     10 1 27 my( $class, $Document ) = @_;
224              
225 10         27 eval {
226 10         36 $class->pdom_strip_pod( $Document );
227 10         51687 $class->pdom_strip_comments( $Document );
228             };
229              
230 10         46397 return 1;
231             }
232              
233             =item $class->pdom_strip_pod( PDOM )
234              
235             Strips Pod documentation from the PDOM.
236              
237             =cut
238              
239 10     10 1 67 sub pdom_strip_pod { $_[1]->prune('PPI::Token::Pod') }
240              
241             =item $class->pdom_strip_comments( PDOM )
242              
243             Strips comments from the PDOM.
244              
245             =cut
246              
247 10     10 1 35 sub pdom_strip_comments { $_[1]->prune('PPI::Token::Comment') }
248              
249             =item $class->get_namespaces_from_pdom( PDOM )
250              
251             Extract the namespaces from the PDOM. It returns a list of package
252             names in the order that it finds them in the PDOM. It does not
253             remove duplicates (do that later if you like).
254              
255             =cut
256              
257             sub get_namespaces_from_pdom {
258 9     9 1 29 my( $class, $Document ) = @_;
259              
260 9         34 my @array = $class->_get_namespaces_from_pdom( $Document );
261 9         20 map { $_->[0] } @array;
  17         40  
262             }
263              
264             =item $class->get_namespaces_and_versions_from_pdom( PDOM )
265              
266             This extracts version information if the package statement uses the
267             Perl 5.12 syntax:
268              
269             package NAME VERSION BLOCK
270              
271             Extract the namespaces from the PDOM. It returns a list anonymous
272             arrays of package names and versions in the order that it finds them
273             in the PDOM. It does not remove duplicates (do that later if you like).
274              
275             =cut
276              
277             sub get_namespaces_and_versions_from_pdom {
278 1     1 1 5 my( $class, $Document ) = @_;
279              
280 1         6 my @array = $class->_get_namespaces_from_pdom( $Document );
281             }
282              
283             sub _get_namespaces_from_pdom {
284 10     10   24 my( $class, $Document ) = @_;
285              
286             my $package_statements = $Document->find(
287             sub {
288             $_[1]->isa('PPI::Statement::Package')
289             ?
290 1684 100   1684   27032 defined eval { $_[1]->namespace }
  21         60  
291             :
292             0
293             }
294 10   100     74 ) || [];
295              
296 10         163 my @namespaces = eval {
297             map {
298             # $1 $2
299 10         25 /package \s+ (\w+(?:::\w+)*) (?:\s* (\S+))? \s* (?:;|\{) /x;
  21         43  
300 21         858 [ $1, $2 ]
301             } @$package_statements
302             };
303              
304             #print STDERR "Got namespaces @namespaces\n";
305              
306 10         35 @namespaces;
307             }
308              
309             =item $class->error
310              
311             Return the error from the last call to C.
312              
313             =cut
314              
315 0         0 BEGIN {
316 3     3   107 my $Error = '';
317              
318 1     1   2 sub _set_error { $Error = $_[1]; }
319              
320 13     13   27 sub _clear_error { $Error = '' }
321              
322 8     8 1 1553 sub error { $Error }
323             }
324              
325             =back
326              
327             =head1 TO DO
328              
329             * Add caching based on file digest?
330              
331             =head1 SOURCE AVAILABILITY
332              
333             This code is in Github:
334              
335             git://github.com/briandfoy/module-extract-namespaces.git
336              
337             =head1 AUTHOR
338              
339             brian d foy, C<< >>
340              
341             This module was partially funded by The Perl Foundation
342             (www.perlfoundation.org) and LogicLAB (www.logiclab.dk), both of whom
343             provided travel assistance to the 2008 Oslo QA Hackathon where I
344             created this module.
345              
346             =head1 COPYRIGHT AND LICENSE
347              
348             Copyright © 2008-2017, brian d foy . All rights reserved.
349              
350             You may redistribute this under the Artistic License 2.0.
351              
352             =cut
353              
354             1;