File Coverage

blib/lib/Pod/Weaver/Plugin/EnsureUniqueSections.pm
Criterion Covered Total %
statement 60 60 100.0
branch 10 12 83.3
condition 1 3 33.3
subroutine 17 17 100.0
pod 2 2 100.0
total 90 94 95.7


line stmt bran cond sub pod time code
1 1     1   87164 use strict;
  1         3  
  1         26  
2 1     1   4 use warnings;
  1         3  
  1         21  
3 1     1   5 use utf8;
  1         2  
  1         7  
4              
5             package Pod::Weaver::Plugin::EnsureUniqueSections;
6             $Pod::Weaver::Plugin::EnsureUniqueSections::VERSION = '0.163250';
7 1     1   43 use Moose;
  1         2  
  1         4  
8 1     1   5704 use MooseX::Has::Sugar;
  1         592  
  1         4  
9 1     1   93 use Moose::Autobox 0.10;
  1         20  
  1         6  
10 1     1   820 use Text::Trim;
  1         435  
  1         52  
11              
12 1     1   237 use Lingua::EN::Inflect::Number qw(to_S);
  1         22827  
  1         5  
13 1     1   162 use Carp;
  1         3  
  1         545  
14             with 'Pod::Weaver::Role::Finalizer';
15             with 'Pod::Weaver::Role::Preparer';
16             # ABSTRACT: Ensure that POD has no duplicate section headers.
17              
18              
19             has strict => (
20             ro, lazy,
21             isa => 'Bool',
22             default => sub { 0 },
23             );
24              
25             sub _header_key {
26 40     40   77 my ($self, $text) = @_;
27 40 100       1375 if (!$self->strict) {
28             # Replace all non-words with a single space
29 36         101 $text =~ s{\W+}{ }xsmg;
30             # Trim leading and trailing whitespace
31 36         101 $text = trim $text;
32             # All to uppercase
33 36         614 $text = uc $text;
34             # Reorder "AND" lists and singularize nouns
35             $text = $text
36             ->split(qr{ AND }i)
37 43 100   43   4739 ->map(sub { m{\W} ? $_ : to_S $_; })
38 36         181 ->sort->join(' AND ');
39             }
40 40         22462 return $text;
41             }
42              
43              
44             sub prepare_input {
45 6     6 1 921337 my $self = shift;
46             # Put EnsureUniqueSections plugins at the end
47 6         160 my $plugins = $self->weaver->plugins;
48 6         178 @$plugins = ((grep { $_ != $self } @$plugins), $self);
  90         167  
49             }
50              
51              
52             sub finalize_document {
53 6     6 1 105532 my ($self, $document) = @_;
54             my $headers = $document->children
55 40 50   40   1353 ->grep(sub{ $_->can( 'command' ) and $_->command eq 'head1' })
56 6     40   136 ->map(sub{ $_->content });
  40         1102  
57 6         58 my %header_group;
58 6         17 for my $h (@$headers) {
59 40         88 push @{$header_group{$self->_header_key($h)}}, $h;
  40         100  
60             }
61              
62             my $duplicate_headers = [ keys %header_group ]
63 36 100   36   121 ->map(sub{ @{$header_group{$_}} > 1 ? $header_group{$_}->head : () })
  36         107  
64 6         55 ->sort;
65 6 100       80 if (@$duplicate_headers > 0) {
66 4         9 my $pod_string = "";
67 4         10 for my $h (@$duplicate_headers) {
68 4         7 for my $node (@{ $document->children->grep(
69             sub {
70 30 50 33 30   1640 $_->can('command') && $_->command eq 'head1' &&
71             $_->content eq $h
72 4         115 }) }) {
73 7         1126 $pod_string .= $node->as_pod_string;
74             }
75             }
76 4         1099 $self->log_debug(["POD of duplicated headers:\n\n%s", $pod_string]);
77 4         361 my $message = "Error: The following headers appear multiple times: '" . $duplicate_headers->join(q{', '}) . q{'};
78 4         45 $self->log_fatal($message);
79             }
80             }
81              
82             1; # Magic true value required at end of module
83              
84             __END__
85              
86             =pod
87              
88             =head1 NAME
89              
90             Pod::Weaver::Plugin::EnsureUniqueSections - Ensure that POD has no duplicate section headers.
91              
92             =head1 VERSION
93              
94             version 0.163250
95              
96             =head1 SYNOPSIS
97              
98             In F<weaver.ini>
99              
100             [-EnsureUniqueSections]
101             strict = 0 ; The default
102              
103             =head1 DESCRIPTION
104              
105             This plugin simply ensures that the POD after weaving has no duplicate
106             top-level section headers. This can help you if you are converting
107             from writing all your own POD to generating it with L<Pod::Weaver>. If
108             you begin generating a section with L<Pod::Weaver> but you forget to
109             delete the manually written section of the same name, this plugin will
110             warn you.
111              
112             By default, this module does some tricks to detect similar headers,
113             such as C<AUTHOR> and C<AUTHORS>. You can turn this off by setting
114             C<strict = 1> in F<weaver.ini>, in which case only I<exactly identical>
115             headers will be considered duplicates of each other.
116              
117             =head2 DIAGNOSTIC MESSAGES
118              
119             If any similar (or identical if C<strict> is 1) section headers are
120             found, all of their names will be listed on STDERR. Generally, you
121             should take this list of modules and remove each from your POD. Then
122             you should ensure that the sections generated by L<Pod::Weaver> are
123             suitable substitutes for those sections. In the case of similar names,
124             only the first instance in each set of similar names will be listed.
125              
126             =head1 ATTRIBUTES
127              
128             =head2 strict
129              
130             If set to true (1), section headers will only be considered duplicates
131             if they match exactly. If false (the default), certain similar section
132             headers will be considered equivalent. The following similarities are
133             considered (more may be added later):
134              
135             =over 4
136              
137             =item All whitespace and punctuation are equivalant
138              
139             For example, the following would all be considered duplicates of each
140             other: C< SEE ALSO>, C<SEE ALSO>, C<SEE,ALSO:>.
141              
142             =item Case-insensitive
143              
144             For example, C<Name> and C<NAME> would be considered duplicates.
145              
146             =item Sets of words separated by "AND".
147              
148             For example, "COPYRIGHT AND LICENSE" would be considered a duplicate
149             of "LICENSE AND COPYRIGHT".
150              
151             =item Plurals
152              
153             A plural noun is considered equivalent to its singular. For example,
154             "AUTHOR" and "AUTHORS" are the same section. A section header
155             consisting of multiple words, such as "DISCLAIMER OF WARRANTY", is not
156             affected by this rule.
157              
158             This rule uses L<Lingua::EN::Inflect::Number> to interconvert between
159             singular and plural forms. Hopefully you don't need to make a section
160             called C<OCTOPI>.
161              
162             =back
163              
164             Note that these rules apply recursively, so C<Authors; and
165             Contributors> would be a duplicate of C< CONTRIBUTORS AND AUTHOR>.
166              
167             =head1 METHODS
168              
169             =head2 prepare_input
170              
171             This method modifies the weaver object by moving EnsureUniqueSections
172             to the end of the weaver's plugin list to ensure that it gets to look
173             at the final woven POD.
174              
175             THIS IS PURE EVIL. This is a hack to ensure that this plugin gets "the
176             last word". Obviously if all plugins used this it would be total
177             chaos. I welcome alternative suggestions. The main issue is that when
178             other Finalizers, such as Section::Leftovers (which happens to be the
179             most likely plugin to create duplicate sections), produce sections,
180             this plugin will only see those sections if it runs after those
181             Finalizers. Hence the need to be the last plugin on the list.
182              
183             =head2 finalize_document
184              
185             This method checks the document for duplicate headers, and throws an
186             error if any are found. If no duplicates are found, it simply does
187             nothing. It does not modify the POD in any way.
188              
189             =head1 BUGS AND LIMITATIONS
190              
191             =head2 Should also be available as a L<Dist::Zilla> testing plugin
192              
193             I would like to convert this to a L<Dist::Zilla> testing plugin, so that
194             you can use it without enabling L<Pod::Weaver> if you don't want to,
195             but I haven't yet figured out how to find all files in a dist with POD
196             and extract all their headers. If anyone knows, please tell me.
197              
198             =head2 No recursive duplicate checks
199              
200             This module only checks for duplicates in top-level headers (i.e.
201             C<head1>). It could be extended to check the C<head2> elements within
202             each C<head1> section and so on, but generally L<Pod::Weaver> is not
203             called upon to generate subsections, so you are unlikely to end up
204             with duplicates at any level other than the first. However, if there
205             is demand for recursive duplicate detection, I will add it.
206              
207             Please report any bugs or feature requests to
208             C<rct+perlbug@thompsonclan.org>.
209              
210             =head1 SEE ALSO
211              
212             =over 4
213              
214             =item *
215              
216             L<Pod::Weaver> - The module that this is a plugin for.
217              
218             =item *
219              
220             L<Lingua::EN::Inflect::Number> - Used to determine the singular forms of plural nouns.
221              
222             =back
223              
224             =head1 INSTALLATION
225              
226             See perlmodinstall for information and options on installing Perl modules.
227              
228             =head1 AUTHOR
229              
230             Ryan C. Thompson <rct@thompsonclan.org>
231              
232             =head1 COPYRIGHT AND LICENSE
233              
234             This software is copyright (c) 2010 by Ryan C. Thompson.
235              
236             This is free software; you can redistribute it and/or modify it under
237             the same terms as the Perl 5 programming language system itself.
238              
239             =head1 DISCLAIMER OF WARRANTY
240              
241             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
242             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT
243             WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER
244             PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND,
245             EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
246             IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
247             PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
248             SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME
249             THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION.
250              
251             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
252             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
253             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE
254             TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR
255             CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
256             SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
257             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
258             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
259             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
260             DAMAGES.
261              
262             =cut