File Coverage

blib/lib/Class/Discover.pm
Criterion Covered Total %
statement 84 89 94.3
branch 22 30 73.3
condition 15 25 60.0
subroutine 13 13 100.0
pod 1 1 100.0
total 135 158 85.4


line stmt bran cond sub pod time code
1             package Class::Discover;
2              
3 1     1   928 use strict;
  1         2  
  1         35  
4 1     1   5 use warnings;
  1         2  
  1         45  
5              
6             our $VERSION = "1.000003";
7              
8 1     1   990 use File::Find::Rule;
  1         9950  
  1         9  
9 1     1   1126 use File::Find::Rule::Perl;
  1         8616  
  1         15  
10 1     1   1291 use PPI;
  1         191297  
  1         44  
11 1     1   13 use File::Temp;
  1         3  
  1         102  
12 1     1   1412 use ExtUtils::MM_Unix;
  1         124417  
  1         39  
13 1     1   13 use Carp qw/croak/;
  1         3  
  1         73  
14 1     1   7 use Path::Class;
  1         2  
  1         897  
15              
16             sub discover_classes {
17 4     4 1 13502 my ($class, $opts) = @_;
18              
19 4   50     18 $opts ||= {};
20 4   50     36 $opts->{keywords} ||= [qw/class role/];
21              
22 4 50 50     25 $opts->{keywords} = [ $opts->{keywords} ]
23             if (ref $opts->{keywords} ||'') ne 'ARRAY';
24              
25 4         5 $opts->{keywords} = { map { $_ => 1 } @{$opts->{keywords}} };
  8         30  
  4         11  
26              
27 4         9 my @files;
28 4   50     36 my $dir = dir($opts->{dir} || "");
29              
30 4 50       395 croak "'dir' option to discover_classes must be absolute"
31             unless $dir->is_absolute;
32              
33 4 50 50     279 if ((ref $opts->{files} || '') eq 'ARRAY') {
    100          
    50          
34 0         0 @files = @{$opts->{files}};
  0         0  
35             }
36             elsif ($opts->{files}) {
37 1         4 @files = ($opts->{files});
38             }
39             elsif ($dir) {
40 3         45 my $rule = File::Find::Rule->new;
41 3         44 my $no_index = $opts->{no_index};
42 0         0 @files = $rule->no_index({
43 3 50       25 directory => [ map { "$dir/$_" } @{$no_index->{directory} || []} ],
  1         5  
44 3 100       7 file => [ map { "$dir/$_" } @{$no_index->{file} || []} ],
  3         30  
45             } )->perl_module
46             ->in($dir)
47             }
48              
49 4 50       6825 croak "Found no files!" unless @files;
50            
51 5         186 return [ map {
52 4         11 my $file = file($_);
53            
54 5         700 local $opts->{file} = $file->relative($dir)->stringify;
55 5         1516 $class->_search_for_classes_in_file($opts, "$file")
56             } @files ];
57             }
58              
59             sub _search_for_classes_in_file {
60 5     5   172 my ($class, $opts, $file) = @_;
61              
62 5         51 my $doc = PPI::Document->new($file);
63              
64 10         29 return map {
65 25         157 $opts->{prefix} = "";
66 10         53 $class->_search_for_classes_in_node($_, $opts);
67             } grep {
68             # Tokens can't have children
69 5         23482 ! $_->isa("PPI::Token")
70             } $doc->children;
71             }
72              
73             sub _search_for_classes_in_node {
74 15     15   40 my ($self, $node, $opts) = @_;
75              
76             my $nodes = $node->find(sub {
77             # Return undef says don't descend
78 114 100 100 114   1949 $_[1]->isa('PPI::Token::Word') && $opts->{keywords}{$_[1]->content}
79             || undef
80 15         122 });
81 15 100       190 return unless $nodes;
82              
83              
84 6         8 my @ret;
85 6         21 for my $n (@$nodes) {
86 10         874 my $type = $n->content;
87 10         77 $n = $n->next_token;
88             # Skip over whitespace
89 10   66     502 $n = $n->next_token while ($n && !$n->significant);
90              
91 10 50 33     363 next unless $n && $n->isa('PPI::Token::Word');
92              
93 10         37 my $class = $n->content;
94              
95 10 100       66 $class = $opts->{prefix} . $class
96             if $class =~ /^::/;
97              
98             # Now look for the '{'
99 10   66     62 $n = $n->next_token while ($n && $n->content ne '{' );
100              
101 10 50       955 unless ($n) {
102 0         0 warn "Unable to find '{' after 'class' somewhere in $opts->{file}\n";
103 0         0 return;
104             }
105              
106 10         63 my $cls = { $class => { file => $opts->{file}, type => $type } };
107 10         16 push @ret, $cls;
108              
109             # $n was the '{' token, its parent is the block/constructor for the 'hash'
110 10         43 $n = $n->parent;
111            
112 10         73 for ($n->children) {
113             # Tokens can't have children
114 30 100       669 next if $_->isa('PPI::Token');
115 5         16 local $opts->{prefix} = $class;
116 5         31 push @ret, $self->_search_for_classes_in_node($_, $opts)
117             }
118              
119             # I dont fancy duplicating the effort of parsing version numbers. So write
120             # the stuff inside {} to a tmp file and use EUMM to get the version number
121             # from it.
122 10         90 my $fh = File::Temp->new;
123 10         5016 $fh->print($n->content);
124 10         732 $fh->close;
125 10         689 my $ver = ExtUtils::MM_Unix->parse_version($fh);
126              
127 10 100 66     6083 $cls->{$class}{version} = $ver if defined $ver && $ver ne "undef";
128              
129             # Remove the block from the parent, so that we dont get confused by
130             # versions of sub-classes
131 10         57 $n->parent->remove_child($n);
132             }
133              
134 6         1550 return @ret;
135             }
136              
137             1;
138              
139             =head1 NAME
140              
141             Class::Discover - detect MooseX::Declare's 'class' keyword in files.
142              
143             =head1 SYNOPSIS
144              
145             =head1 DESCRIPTION
146              
147             This class is designed primarily for tools that whish to populate the
148             C field of META.{yml,json} files so that the CPAN indexer will pay
149             attention to the existance of your classes, rather than blithely ignoring them.
150              
151             The version parsing is basically the same as what M::I's C<< ->version_form >>
152             does, so should hopefully work as well as it does.
153              
154             =head1 METHODS
155              
156             =head2 discover_classes
157              
158             Class::Discover->discover_classes(\%opts)
159              
160             Takes a single options hash-ref, and returns a array-ref of hashes with the
161             following format:
162              
163             { MyClass => { file => "lib/MtClass.pm", type => "class", version => "1" } }
164              
165             C will only be present if the class has a (detected) version.
166             C is the C match that triggered this class.
167              
168             The following options are understood:
169              
170             =over
171              
172             =item dir
173              
174             The (absolute) directory from which files should be given relative to. If
175             C is not passed, then the dir under which to search for modules.
176              
177             =item files
178              
179             Array-ref of files in which to look. If provided, then only these files will be
180             searched.
181              
182             =item keywords
183              
184             List of 'keywords' which are treated as being class declarators. Defaults to
185             C and C.
186              
187             =item no_index
188              
189             A hash of arrays with keys of C and C which are ignored when
190             searching for packages.
191              
192             =back
193              
194             =head1 SEE ALSO
195              
196             L for the main reason for this module to exist.
197              
198             L
199              
200             L
201              
202             =head1 AUTHOR
203              
204             Ash Berlin C<< >>. (C) 2009. All rights reserved.
205              
206             =head1 LICENSE
207              
208             Licensed under the same terms as Perl itself.
209