File Coverage

blib/lib/Perl/Critic/Policy/Miscellanea/TextDomainPlaceholders.pm
Criterion Covered Total %
statement 122 125 97.6
branch 62 64 96.8
condition 31 45 68.8
subroutine 16 16 100.0
pod 1 1 100.0
total 232 251 92.4


line stmt bran cond sub pod time code
1             # Copyright 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 Kevin Ryde
2              
3             # This file is part of Perl-Critic-Pulp.
4              
5             # Perl-Critic-Pulp is free software; you can redistribute it and/or modify
6             # it under the terms of the GNU General Public License as published by the
7             # Free Software Foundation; either version 3, or (at your option) any later
8             # version.
9             #
10             # Perl-Critic-Pulp is distributed in the hope that it will be useful, but
11             # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12             # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13             # for more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with Perl-Critic-Pulp. If not, see <http://www.gnu.org/licenses/>.
17              
18              
19             package Perl::Critic::Policy::Miscellanea::TextDomainPlaceholders;
20 40     40   1070 use 5.006;
  40         201  
21 40     40   207 use strict;
  40         214  
  40         949  
22 40     40   216 use warnings;
  40         141  
  40         2558  
23              
24 40     40   306 use base 'Perl::Critic::Policy';
  40         145  
  40         3992  
25 40         3545 use Perl::Critic::Utils qw(is_function_call
26             parse_arg_list
27 40     40   144279 interpolate);
  40         209  
28              
29             # uncomment this to run the ### lines
30             # use Smart::Comments;
31              
32             our $VERSION = 97;
33              
34 40     40   230 use constant supported_parameters => ();
  40         83  
  40         2168  
35 40     40   217 use constant default_severity => $Perl::Critic::Utils::SEVERITY_MEDIUM;
  40         177  
  40         1928  
36 40     40   212 use constant default_themes => qw(pulp bugs);
  40         71  
  40         1980  
37 40     40   220 use constant applies_to => 'PPI::Token::Word';
  40         81  
  40         49183  
38              
39             my %funcs = (__x => 1,
40             __nx => 1,
41             __xn => 1,
42              
43             __px => 1,
44             __npx => 1);
45              
46             sub violates {
47 83     83 1 673793 my ($self, $elem, $document) = @_;
48              
49 83         214 my $funcname = $elem->content;
50 83         304 $funcname =~ s/^Locale::TextDomain:://;
51 83 100       225 $funcs{$funcname} || return;
52             ### TextDomainPlaceholders: $elem->content
53              
54 49 100       124 is_function_call($elem) || return;
55              
56 42         8675 my @violations;
57              
58             # The arg crunching bits assume one parsed expression results in one arg,
59             # which is not true if the expressions are an array, a hash, or a function
60             # call returning multiple values. The one-arg-one-value assumption is
61             # reasonable on the whole though.
62             #
63             # In the worst case you'd have to take any function call value part like
64             # "foo => FOO()" to perhaps return multiple values -- which would
65             # completely defeat testing of normal cases, so don't want to do that.
66             #
67             # ENHANCE-ME: One bit that could be done though is to recognise a %foo arg
68             # as giving an even number of values, so keyword checking could continue
69             # past it.
70              
71             # each element of @args is an arrayref containing PPI elements making up
72             # the arg
73 42         103 my @args = parse_arg_list ($elem);
74             ### got total arg count: scalar(@args)
75              
76 42 100       5284 if ($funcname =~ /p/) {
77             # msgctxt context arg to __p, __npx
78 4         10 shift @args;
79             }
80              
81             # one format to __x, two to __nx and other "n" funcs
82 42 100       134 my @format_args = splice @args, 0, ($funcname =~ /n/ ? 2 : 1);
83              
84 42 100       99 if ($funcname =~ /n/) {
85             # count arg to __nx and other "n" funcs
86 17         29 my $count_arg = shift @args;
87 17 100 100     52 if (! $count_arg
88             || do {
89             # if it looks like a keyword symbol foo=> or 'foo' etc
90 14         31 my ($str, $any_vars) = _arg_word_or_string ($count_arg, $document);
91 14 100       92 ($str =~ /^[[:alpha:]_]\w*$/ && ! $any_vars)
92             }) {
93 8   66     52 push @violations, $self->violation
94             ("Probably missing 'count' argument to $funcname",
95             '',
96             $count_arg->[0] || $elem);
97             }
98             }
99              
100             ### got data arg count: scalar(@args)
101              
102 42         1496 my $args_any_vars = 0;
103 42         61 my %arg_keys;
104 42         93 while (@args) {
105 38         58 my $arg = shift @args;
106 38         73 my ($str, $any_vars) = _arg_word_or_string ($arg, $document);
107 38   100     214 $args_any_vars ||= $any_vars;
108             ### arg: @$arg
109             ### $str
110             ### $any_vars
111 38 100       78 if (! $any_vars) {
112 31         68 $arg_keys{$str} = $arg;
113             }
114 38         92 shift @args; # value part
115             }
116              
117 42         70 my %format_keys;
118             my $format_any_vars;
119              
120 42         68 foreach my $format_arg (@format_args) {
121 59         1244 my ($format_str, $any_vars) = _arg_string ($format_arg, $document);
122 59   66     210 $format_any_vars ||= $any_vars;
123              
124 59         275 while ($format_str =~ /\{(\w+)\}/g) {
125 50         114 my $format_key = $1;
126             ### $format_key
127 50         93 $format_keys{$format_key} = 1;
128              
129 50 100 100     201 if (! $args_any_vars && ! exists $arg_keys{$format_key}) {
130 21   33     127 push @violations, $self->violation
131             ("Format key '$format_key' not in arg list",
132             '',
133             $format_arg->[0] || $elem);
134             }
135             }
136             }
137              
138 42 100       2395 if (! $format_any_vars) {
139 39         90 foreach my $arg_key (keys %arg_keys) {
140 28 100       223 if (! exists $format_keys{$arg_key}) {
141 13         24 my $arg = $arg_keys{$arg_key};
142 13 100 33     92 push @violations, $self->violation
143             ("Argument key '$arg_key' not used by format"
144             . (@format_args == 1 ? '' : 's'),
145             '',
146             $arg->[0] || $elem);
147             }
148             }
149             }
150             ### total violation count: scalar(@violations)
151              
152 42         2094 return @violations;
153             }
154              
155             sub _arg_word_or_string {
156 52     52   92 my ($arg, $document) = @_;
157 52 100 66     263 if (@$arg == 1 && $arg->[0]->isa('PPI::Token::Word')) {
158 27         73 return ("$arg->[0]", 0);
159             } else {
160 25         53 return _arg_string ($arg, $document);
161             }
162             }
163              
164             # $arg is an arrayref of PPI::Element which are an argument
165             # if it's a constant string or "." concat of such then
166             # return ($str, $any_vars) where $str is the string content
167             # and $any_vars is true if there's any variables to be interpolated in $str
168             #
169             sub _arg_string {
170 190     190   349 my ($arg, $document) = @_;
171             ### _arg_string() ...
172              
173 190         362 my @elems = @$arg;
174 190         297 my $ret = '';
175 190         285 my $any_vars = 0;
176              
177 190         365 while (@elems) {
178 202         466 my $elem = shift @elems;
179              
180 202 100       958 if ($elem->isa('PPI::Token::Quote')) {
    100          
    100          
    100          
181 85         249 my $str = $elem->string;
182 85 100 100     840 if ($elem->isa('PPI::Token::Quote::Double')
183             || $elem->isa('PPI::Token::Quote::Interpolate')) {
184             # ENHANCE-ME: use $arg->interpolations() when available also on
185             # PPI::Token::Quote::Interpolate
186 22   66     72 $any_vars ||= _string_any_vars ($str);
187             }
188 85         175 $ret .= $str;
189              
190             } elsif ($elem->isa('PPI::Token::HereDoc')) {
191 4         13 my $str = join('',$elem->heredoc);
192 4 50       25 if ($elem =~ /`$/) {
    100          
193 0         0 $str = ' '; # no idea what running backticks might produce
194 0         0 $any_vars = 1;
195             } elsif ($elem !~ /'$/) {
196             # explicit "HERE" or default HERE expand vars
197 3   66     30 $any_vars ||= _string_any_vars ($str);
198             }
199 4         16 $ret .= $str;
200              
201             } elsif ($elem->isa('PPI::Token::Number')) {
202             ### number can work like a constant string ...
203 10         31 $ret .= $elem->content;
204              
205             } elsif ($elem->isa('PPI::Token::Word')) {
206             ### word ...
207 88         136 my $next;
208 88 100 66     189 if ($elem eq '__PACKAGE__') {
    100 66        
    100          
    100          
209 8         99 $ret .= _elem_package_name($elem);
210              
211             } elsif ($elem eq '__LINE__') {
212             ### logical line: $elem->location->[3]
213 8         174 $ret .= $elem->location->[3]; # logical line using any #line directives
214              
215             } elsif ($elem eq '__FILE__') {
216 4         108 my $filename = _elem_logical_filename($elem,$document);
217 4 100       28 if (! defined $filename) {
218 2         3 $filename = 'unknown-filename.pl';
219             }
220             ### $filename
221 4         12 $ret .= $filename;
222              
223             } elsif (($next = $elem->snext_sibling)
224             && $next->isa('PPI::Token::Operator')
225             && $next eq '=>') {
226             ### word quoted by => ...
227 64         4094 $ret .= $elem->content;
228 64         244 last;
229             } else {
230             ### some function call or something ...
231 4         243 return ('', 2);
232             }
233              
234             } else {
235             ### some variable or expression or something ...
236 15         52 return ('', 2);
237             }
238              
239              
240 119 100       353 if (! @elems) { last; }
  107         190  
241 12         25 my $op = shift @elems;
242 12 50 33     49 if (! ($op->isa('PPI::Token::Operator') && $op eq '.')) {
243             # something other than "." concat
244 0         0 return ('', 2);
245             }
246             }
247 171         508 return ($ret, $any_vars);
248             }
249              
250             # $str is the contents of a "" or qq{} string
251             # return true if it has any $ or @ interpolation forms
252             sub _string_any_vars {
253 42     42   4621 my ($str) = @_;
254 42         250 return ($str =~ /(^|[^\\])(\\\\)*[\$@]/);
255             }
256              
257             # $elem is a PPI::Element
258             # Return the name (a string) of its containing package, or "main" if not
259             # under any package statement.
260             #
261             sub _elem_package_name {
262 8     8   16 my ($elem) = @_;
263 8 100       20 if (my $packelem = Perl::Critic::Pulp::Utils::elem_package($elem)) {
264 3 100       12 if (my $name = $packelem->namespace) {
265 1         26 return $name;
266             }
267             }
268 7         98 return 'main';
269             }
270              
271             # As per perlsyn.pod, except \2 instead of \g2 since \g only in perl 5.10 up.
272             # Is this in a module somewhere?
273             my $line_directive_re =
274             qr/^\# \s*
275             line \s+ (\d+) \s*
276             (?:\s("?)([^"]+)\2)? \s*
277             $/xm;
278              
279             # $elem is a PPI::Element
280             # Return its logical filename (a string).
281             # This is from a "#line" comment directive, or the $document filename if no
282             # such.
283             #
284             sub _elem_logical_filename {
285 4     4   9 my ($elem, $document) = @_;
286             ### _elem_logical_filename(): "$elem"
287              
288 4         6 my $filename;
289             $document->find_first (sub {
290 116     116   981 my ($doc, $e) = @_;
291             # ### comment: (ref $e)." ".$e->content
292 116 100       199 if ($e == $elem) {
293             ### not found before target elem, stop ...
294 4         20 return undef;
295             }
296 112 100 66     565 if ($e->isa('PPI::Token::Comment')
297             && $e->content =~ $line_directive_re) {
298 2         26 $filename = $3;
299             ### found line directive: $filename
300             }
301 112         194 return 0; # continue
302 4         26 });
303 4 100       50 if (defined $filename) {
304 2         6 return $filename;
305             } else {
306             ### not found, use document: $document->filename
307 2         8 return $document->filename;
308             }
309             }
310              
311             1;
312             __END__
313              
314             =for stopwords args arg Gettext Charset runtime Ryde unexpanded
315              
316             =head1 NAME
317              
318             Perl::Critic::Policy::Miscellanea::TextDomainPlaceholders - check placeholder names in Locale::TextDomain calls
319              
320             =head1 DESCRIPTION
321              
322             This policy is part of the L<C<Perl::Critic::Pulp>|Perl::Critic::Pulp>
323             add-on. It checks the placeholder arguments in format strings to the
324             following functions from C<Locale::TextDomain>.
325              
326             __x __nx __xn __px __npx
327              
328             Calls with a key missing from the args or args unused by the format are
329             reported.
330              
331             print __x('Searching for {data}', # bad
332             datum => 123);
333              
334             print __nx('Read one file',
335             'Read {num} files', # bad
336             $n,
337             count => 123);
338              
339             This is normally a mistake, so this policy is under the "bugs" theme (see
340             L<Perl::Critic/POLICY THEMES>). An error can easily go unnoticed because
341             (as of Locale::TextDomain version 1.16) a placeholder without a
342             corresponding arg goes through unexpanded and any extra args are ignored.
343              
344             The way Locale::TextDomain parses the format string allows anything between
345             S<< C<< { } >> >> as a key, but for the purposes of this policy only symbols
346             (alphanumeric plus "_") are taken to be a key. This is almost certainly
347             what you'll want to use, and it's then possible to include literal braces in
348             a format string without tickling this policy all the time. (Symbol
349             characters are per Perl C<\w>, so non-ASCII is supported, though the Gettext
350             manual in node "Charset conversion" recommends message-IDs should be
351             ASCII-only.)
352              
353             =head1 Partial Checks
354              
355             If the format string is not a literal then it might use any args, so all are
356             considered used.
357              
358             # ok, 'datum' might be used
359             __x($my_format, datum => 123);
360              
361             Literal portions of the format are still checked.
362              
363             # bad, 'foo' not present in args
364             __x("{foo} $bar", datum => 123);
365              
366             Conversely if the args have some non-literals then they could be anything,
367             so everything in the format string is considered present.
368              
369             # ok, $something might be 'world'
370             __x('hello {world}', $something => 123);
371              
372             But again if some args are literals they can be checked.
373              
374             # bad, 'blah' is not used
375             __x('hello {world}', $something => 123, blah => 456);
376              
377             If there's non-literals both in the format and in the args then nothing is
378             checked, since it could all match up fine at runtime.
379              
380             =head2 C<__nx> Count Argument
381              
382             A missing count argument to C<__nx>, C<__xn> and C<__npx> is sometimes
383             noticed by this policy. For example,
384              
385             print __nx('Read one file',
386             'Read {numfiles} files',
387             numfiles => $numfiles); # bad
388              
389             If the count argument looks like a key then it's reported as a probable
390             mistake. This is not the main aim of this policy but it's done because
391             otherwise no violations would be reported at all. (The next argument would
392             be the key, and normally being an expression it would be assumed to fulfill
393             the format strings at runtime.)
394              
395             =head1 SEE ALSO
396              
397             L<Perl::Critic::Pulp>,
398             L<Perl::Critic>,
399             L<Locale::TextDomain>,
400             L<Perl::Critic::Policy::Miscellanea::TextDomainUnused>
401              
402             =head1 HOME PAGE
403              
404             L<http://user42.tuxfamily.org/perl-critic-pulp/index.html>
405              
406             =head1 COPYRIGHT
407              
408             Copyright 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 Kevin Ryde
409              
410             Perl-Critic-Pulp is free software; you can redistribute it and/or modify it
411             under the terms of the GNU General Public License as published by the Free
412             Software Foundation; either version 3, or (at your option) any later
413             version.
414              
415             Perl-Critic-Pulp is distributed in the hope that it will be useful, but
416             WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
417             or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
418             more details.
419              
420             You should have received a copy of the GNU General Public License along with
421             Perl-Critic-Pulp. If not, see <http://www.gnu.org/licenses/>.
422              
423             =cut