File Coverage

blib/lib/MooseX/amine.pm
Criterion Covered Total %
statement 166 166 100.0
branch 60 64 93.7
condition 1 2 50.0
subroutine 27 27 100.0
pod 2 2 100.0
total 256 261 98.0


line stmt bran cond sub pod time code
1             package MooseX::amine;
2             # ABSTRACT: Examine Yr Moose
3             $MooseX::amine::VERSION = '0.7';
4 14     14   954313 use Moose;
  14         6295036  
  14         98  
5 14     14   97211 use Moose::Meta::Class;
  14         30  
  14         321  
6 14     14   71 use Moose::Meta::Role;
  14         26  
  14         431  
7 14     14   84 use Moose::Util::TypeConstraints;
  14         27  
  14         146  
8              
9 14     14   29097 use 5.010;
  14         47  
10 14     14   9038 use autodie qw(open close);
  14         213200  
  14         74  
11 14     14   14340 use PPI;
  14         1450723  
  14         655  
12 14     14   6202 use Test::Deep::NoTest qw/eq_deeply/;
  14         123024  
  14         110  
13 14     14   2853 use Try::Tiny;
  14         34  
  14         32572  
14              
15              
16             has 'include_accessors_in_method_list' => (
17             is => 'ro' ,
18             isa => 'Bool' ,
19             default => 0 ,
20             );
21              
22             has 'include_moose_in_isa' => (
23             is => 'ro' ,
24             isa => 'Bool' ,
25             default => 0 ,
26             );
27              
28             has 'include_private_attributes' => => (
29             is => 'ro' ,
30             isa => 'Bool' ,
31             default => 0 ,
32             );
33              
34             has 'include_private_methods' => => (
35             is => 'ro' ,
36             isa => 'Bool' ,
37             default => 0 ,
38             );
39              
40             has 'include_standard_methods' => (
41             is => 'ro' ,
42             isa => 'Bool' ,
43             default => 0 ,
44             );
45              
46             has 'module' => ( is => 'ro' , isa => 'Str' );
47             has 'path' => ( is => 'ro' , isa => 'Str' );
48              
49             has '_attributes' => (
50             is => 'ro' ,
51             isa => 'HashRef' ,
52             traits => [ 'Hash' ] ,
53             handles => {
54             _get_attribute => 'get' ,
55             _store_attribute => 'set' ,
56             _check_for_stored_attribute => 'exists' ,
57             },
58             );
59              
60             has '_exclusions' => (
61             is => 'ro' ,
62             isa => 'HashRef' ,
63             handles => {
64 51     51   117 _add_exclusion => sub { my( $self , $ex ) = @_; $self->{_exclusions}{$ex}++ } ,
  51         173  
65 143     143   249 _check_exclusion => sub { my( $self , $ex ) = @_; return $self->{_exclusions}{$ex} } ,
  143         430  
66             }
67             );
68              
69             has '_metaobj' => (
70             is => 'ro' ,
71             isa => 'Object' ,
72             lazy => 1 ,
73             builder => '_build_metaobj' ,
74             );
75              
76             sub _build_metaobj {
77 14     14   29 my $self = shift;
78             return $self->{module}->meta
79 14   50     85 || die "Can't get meta object for module!" ;
80             }
81              
82             has '_methods' => (
83             is => 'ro' ,
84             isa => 'HashRef' ,
85             traits => [ 'Hash' ] ,
86             handles => {
87             _store_method => 'set' ,
88             },
89             );
90              
91             has '_sub_nodes' => (
92             is => 'ro' ,
93             isa => 'HashRef' ,
94             traits => [ 'Hash' ] ,
95             handles => {
96             _get_sub_node => 'get' ,
97             _store_sub_node => 'set' ,
98             },
99             );
100              
101             sub BUILDARGS {
102 18     18 1 18133 my $class = shift;
103              
104 18         81 my $args = _convert_to_hashref_if_needed( @_ );
105              
106 18 100       77 if ( $args->{module}) {
    100          
107 13         886 eval "require $args->{module};";
108 13 100       647015 die $@ if $@;
109              
110 12         65 my $path = $args->{module} . '.pm';
111 12         65 $path =~ s|::|/|g;
112 12         57 $args->{path} = $INC{$path};
113             }
114             elsif ( $args->{path} ) {
115 4         32 open( my $IN , '<' , $args->{path} );
116 3         2440 while (<$IN>) {
117 4 100       33 if ( /^package ([^;]+);/ ) {
118 3         14 my $module = $1;
119 3         12 $args->{module} = _load_module_from_path( $module , $args->{path} );
120 2         6 last;
121             }
122             }
123 2         9 close( $IN );
124             }
125 1         10 else { die "Need to provide 'module' or 'path'" }
126 14         1327 return $args;
127             }
128              
129              
130             sub examine {
131 14     14 1 56667 my $self = shift;
132 14         454 my $meta = $self->_metaobj;
133              
134 14 100       98 if ( $meta->isa( 'Moose::Meta::Role' )) {
135 1         3 $self->_dissect_role( $meta );
136             }
137             else {
138 13         61 foreach my $class ( reverse $meta->linearized_isa ) {
139 33 100       9631 if ( $class =~ /^Moose::/) {
140 13 100       430 next unless $self->include_moose_in_isa;
141             }
142 21         102 $self->_dissect_class( $class );
143             }
144             }
145              
146             # Now that we've dissected everything, load the extracted sub nodes into the
147             # appropriate methods
148 14         8849 foreach ( keys %{ $self->{_methods} } ) {
  14         79  
149 42         1396 $self->{_methods}{$_}{code} = $self->_get_sub_node( $_ );
150             }
151              
152             return {
153             attributes => $self->{_attributes} ,
154             methods => $self->{_methods} ,
155             }
156 14         158 }
157              
158             # given two attribute data structures, compare them. returns the older one if
159             # they're the same; the newer one if they're not.
160             #
161             # ignores the value of the 'from' key, since the point here is to check if two
162             # attributes from different packages are otherwise identical.
163             sub _compare_attributes {
164 14     14   34 my( $new_attr , $old_attr ) = @_;
165              
166 14         37 my $new_from = delete $new_attr->{from};
167 14         31 my $old_from = delete $old_attr->{from};
168              
169 14 100       77 if ( eq_deeply( $new_attr , $old_attr )) {
170 10         62947 $old_attr->{from} = $old_from;
171 10         40 return $old_attr;
172             }
173             else {
174 4         25845 $new_attr->{from} = $new_from;
175 4         14 return $new_attr;
176             }
177             }
178              
179             # given a list of args that may or may not be a hashref, do whatever munging
180             # is needed to return a hashref.
181             sub _convert_to_hashref_if_needed {
182 18     18   56 my( @list_of_args ) = @_;
183              
184 18 100       76 return $_[0] if ref $_[0];
185              
186 14 100       77 return { module => $_[0] } if @_ == 1;
187              
188 2         5 my %hash = @_;
189 2         8 return \%hash;
190             }
191              
192             # given a meta object and an attribute name (that is an attribute of that meta
193             # object), extract a bunch of info about it and store it in the _attributes
194             # attr.
195             sub _dissect_attribute {
196 54     54   125 my( $self , $meta , $attribute_name ) = @_;
197              
198 54 100       180 if ( $attribute_name =~ /^_/ ) {
199 4 100       130 return unless $self->include_private_attributes;
200             }
201              
202 51         221 my $meta_attr = $meta->get_attribute( $attribute_name );
203              
204 51         392 my $return;
205 51         139 my $ref = ref $meta_attr;
206 51 100       140 if ( $ref eq 'Moose::Meta::Role::Attribute' ) {
207 11         61 $return = $meta_attr->original_role->name;
208 11         792 $meta_attr = $meta_attr->attribute_for_class();
209             }
210             else {
211 40         187 $return = $meta_attr->associated_class->name
212             }
213              
214 51         8264 my $extracted_attribute = $self->_extract_attribute_metainfo( $meta_attr );
215 51         147 $extracted_attribute->{from} = $return;
216              
217 51 100       1649 if ( $self->_check_for_stored_attribute( $attribute_name )) {
218 14         404 $extracted_attribute = _compare_attributes(
219             $extracted_attribute , $self->_get_attribute( $attribute_name )
220             );
221             }
222              
223 51         1815 $self->_store_attribute( $attribute_name => $extracted_attribute );
224             }
225              
226             # given a class name, extract and store info about it and any roles that it
227             # has consumed.
228             sub _dissect_class {
229 21     21   59 my( $self , $class ) = @_;
230 21         138 my $meta = $class->meta;
231              
232 21 50       628 map { $self->_dissect_role($_) } @{ $meta->roles } if ( $meta->can( 'roles' ));
  9         106  
  21         722  
233 21         4242 map { $self->_dissect_attribute( $meta , $_ ) } $meta->get_attribute_list;
  43         320  
234 21         290 map { $self->_dissect_method( $meta , $_ ) } $meta->get_method_list;
  147         18051  
235              
236 21         140 $self->_extract_sub_nodes( $meta->name );
237             }
238              
239             # given a meta object and a method name (that is a method of that meta
240             # object), extract and store info about the method.
241             sub _dissect_method {
242 167     167   389 my( $self , $meta , $method_name ) = @_;
243              
244 167 100       403 if ( $method_name =~ /^_/ ) {
245 8 100       253 return unless $self->include_private_methods;
246             }
247              
248 161         450 my $meta_method = $meta->get_method( $method_name );
249              
250 161         5182 my $src = $meta_method->original_package_name;
251              
252 161 100       5882 unless ( $self->include_accessors_in_method_list ) {
253 143 100       305 return if $self->_check_exclusion( $method_name );
254             }
255              
256 125 100       2999 unless ( $self->include_standard_methods ) {
257 107         265 my @STOCK = qw/ DESTROY meta new /;
258 107         200 foreach ( @STOCK ) {
259 255 100       554 return if $method_name eq $_;
260             }
261             }
262              
263 59         166 my $extracted_method = $self->_extract_method_metainfo( $meta_method );
264 59         2311 $self->_store_method( $method_name => $extracted_method );
265             }
266              
267             # extract and store information from a particular role
268             sub _dissect_role {
269 10     10   31 my( $self , $meta ) = @_;
270              
271 10         54 map { $self->_dissect_attribute( $meta , $_ ) } $meta->get_attribute_list;
  11         124  
272 10         106 map { $self->_dissect_method( $meta , $_ ) } $meta->get_method_list;
  20         995  
273              
274 10         87 my @names = split '\|' , $meta->name;
275 10         36 foreach my $name ( @names ) {
276 11 50       398 next if $name =~ /Moose::Meta::Role::__ANON/;
277 11         35 $self->_extract_sub_nodes( $name );
278             }
279             }
280              
281             # given a meta attribute, extract a bunch of meta info and return a data
282             # structure summarizing it.
283             sub _extract_attribute_metainfo {
284 51     51   111 my( $self , $meta_attr ) = @_;
285              
286 51         95 my $return = {};
287              
288 51         122 foreach ( qw/ reader writer accessor / ) {
289 153 100       652 next unless my $fxn = $meta_attr->$_;
290 51         185 $self->_add_exclusion( $fxn );
291 51         146 $return->{$_} = $fxn;
292             }
293              
294 51 100       2145 $return->{meta}{documentation} = $meta_attr->documentation
295             if ( $meta_attr->has_documentation );
296              
297 51 100       2853 $return->{meta}{constraint} = $meta_attr->type_constraint->name
298             if ( $meta_attr->has_type_constraint );
299              
300 51 100       4635 $return->{meta}{traits} = $meta_attr->applied_traits
301             if ( $meta_attr->has_applied_traits );
302              
303 51         648 foreach ( qw/
304             is_weak_ref is_required is_lazy is_lazy_build should_coerce
305             should_auto_deref has_trigger has_handles
306             / ) {
307 408 100       14833 $return->{meta}{$_}++ if $meta_attr->$_ ;
308             }
309              
310             ### FIXME should look at delegated methods and install exclusions for them
311              
312 51         313 return $return;
313              
314             }
315              
316             # given a meta method, extract a bunch of info and return a data structure
317             # summarizing it.
318             sub _extract_method_metainfo {
319 59     59   113 my( $self , $meta_method ) = @_;
320              
321             return {
322 59         209 from => $meta_method->original_package_name ,
323             };
324             }
325              
326             # given a module name, use PPI to extract the 'sub' nodes and store them.
327             sub _extract_sub_nodes {
328 32     32   86 my( $self , $name ) = @_;
329              
330 32         94 my $path = $name . '.pm';
331 32         144 $path =~ s|::|/|g;
332 32 50       166 if ( $path = $INC{$path} ){
333             try {
334 32 50   32   1154 my $ppi = PPI::Document->new( $path )
335             or die "Can't load PPI for $path ($!)";
336              
337             my $sub_nodes = $ppi->find(
338 3886 100       41552 sub{ $_[1]->isa( 'PPI::Statement::Sub' ) && $_[1]->name }
339 32         441810 );
340              
341 32         531 foreach my $sub_node ( @$sub_nodes ) {
342 46         158 my $name = $sub_node->name;
343 46         997 $self->_store_sub_node( $name => $sub_node->content );
344             }
345 32         311 };
346             # FIXME should probably do something about errors here...
347             }
348             }
349              
350              
351             # given a module name and a path to that module, dynamically load the
352             # module. figures out the appropriate 'use lib' statement based on the path.
353             sub _load_module_from_path {
354 3     3   12 my( $module , $path ) = @_;
355              
356 3         18 $path =~ s/.pm$//;
357 3         20 my @path_parts = split '/' , $path;
358 3         14 my @module_parts = split /::/ , $module;
359 3         7 my @inc_path = ();
360              
361 3         10 while ( @path_parts ) {
362 14         31 my $path = join '/' , @path_parts;
363 14         23 my $mod = join '/' , @module_parts;
364 14 100       32 last if $path eq $mod;
365 12         28 push @inc_path , shift @path_parts;
366             }
367 3         11 my $inc_path = join '/' , @inc_path;
368              
369 3     2   325 eval "use lib '$inc_path'; require $module";
  2     1   19  
  2         4  
  2         15  
  1         18  
  1         2  
  1         11  
370 3 100       51275 die $@ if $@;
371              
372 2         15 return $module;
373             }
374              
375              
376             #__PACKAGE__->meta->make_immutable;
377             1;
378              
379             __END__
380              
381             =pod
382              
383             =encoding UTF-8
384              
385             =head1 NAME
386              
387             MooseX::amine - Examine Yr Moose
388              
389             =head1 VERSION
390              
391             version 0.7
392              
393             =head1 SYNOPSIS
394              
395             my $mex = MooseX::amine->new( 'MooseX::amine' );
396             my $data = $mex->examine;
397              
398             my $attributes = $data->{attributes};
399             my $methods = $data->{methods};
400              
401             =head1 METHODS
402              
403             =head2 new
404              
405             # these two are the same
406             my $mex = MooseX::amine->new( 'Module' );
407             my $mex = MooseX::amine->new({ module => 'Module' });
408              
409             # or you can go from the path to the file
410             my $mex = MooseX::amine->new({ path = 'path/to/Module.pm' });
411              
412             # there are a number of options that all pretty much do what they say.
413             # they all default to off
414             my $mex = MooseX::amine->new({
415             module => 'Module' ,
416             include_accessors_in_method_list => 1,
417             include_moose_in_isa => 1,
418             include_private_attributes => 1,
419             include_private_methods => 1,
420             include_standard_methods => 1,
421             });
422              
423             =head2 examine
424              
425             my $mex = MooseX::amine( 'Module' );
426             my $data = $mex->examine();
427              
428             Returns a multi-level hash-based data structure, with two top-level keys,
429             C<attributes> and C<methods>. C<attributes> points to a hash where the keys
430             are attribute names and the values are data structures that describe the
431             attributes. Similarly, C<methods> points to a hash where the keys are method
432             names and the values are data structures describing the method.
433              
434             A sample attribute entry:
435              
436             simple_attribute => {
437             accessor => 'simple_attribute',
438             from => 'Module',
439             meta => {
440             constraint => 'Str'
441             }
442             }
443              
444             The prescence of an C<accessor> key indicates that this attribute was defined
445             with C<is => 'rw'>. A read-only attribute will have a C<reader> key. A
446             C<writer> key may also be present if a specific writer method was given when
447             creating the attribute.
448              
449             Depending on the options given when creating the attribute there may be
450             various other options present under the C<meta> key.
451              
452             A sample method entry:
453              
454             simple_method => {
455             code => 'sub simple_method { return \'simple\' }',
456             from => 'Module'
457             }
458              
459             The C<code> key will contain the actual code from the method, extracted with
460             PPI. Depending on where the method code actually lives, this key may or may
461             not be present.
462              
463             =head1 CREDITS
464              
465             =over 4
466              
467             =item Semi-inspired by L<MooseX::Documenter>.
468              
469             =item Syntax highlighting Javascript/CSS stuff based on SHJS and largely stolen from search.cpan.org.
470              
471             =back
472              
473             =head1 AUTHOR
474              
475             John SJ Anderson <john@genehack.org>
476              
477             =head1 COPYRIGHT AND LICENSE
478              
479             This software is copyright (c) 2020 by John SJ Anderson.
480              
481             This is free software; you can redistribute it and/or modify it under
482             the same terms as the Perl 5 programming language system itself.
483              
484             =cut