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, 2019 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   1371 use 5.006;
  40         267  
21 40     40   424 use strict;
  40         189  
  40         929  
22 40     40   318 use warnings;
  40         1468  
  40         1377  
23              
24 40     40   334 use base 'Perl::Critic::Policy';
  40         132  
  40         5028  
25 40         3254 use Perl::Critic::Utils qw(is_function_call
26             parse_arg_list
27 40     40   180621 interpolate);
  40         200  
28              
29             # uncomment this to run the ### lines
30             # use Smart::Comments;
31              
32             our $VERSION = 98;
33              
34 40     40   295 use constant supported_parameters => ();
  40         127  
  40         2717  
35 40     40   325 use constant default_severity => $Perl::Critic::Utils::SEVERITY_MEDIUM;
  40         122  
  40         2455  
36 40     40   265 use constant default_themes => qw(pulp bugs);
  40         88  
  40         2367  
37 40     40   261 use constant applies_to => 'PPI::Token::Word';
  40         123  
  40         59372  
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 722246 my ($self, $elem, $document) = @_;
48              
49 83         276 my $funcname = $elem->content;
50 83         389 $funcname =~ s/^Locale::TextDomain:://;
51 83 100       275 $funcs{$funcname} || return;
52             ### TextDomainPlaceholders: $elem->content
53              
54 49 100       149 is_function_call($elem) || return;
55              
56 42         10521 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         116 my @args = parse_arg_list ($elem);
74             ### got total arg count: scalar(@args)
75              
76 42 100       6551 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       159 my @format_args = splice @args, 0, ($funcname =~ /n/ ? 2 : 1);
83              
84 42 100       129 if ($funcname =~ /n/) {
85             # count arg to __nx and other "n" funcs
86 17         41 my $count_arg = shift @args;
87 17 100 100     59 if (! $count_arg
88             || do {
89             # if it looks like a keyword symbol foo=> or 'foo' etc
90 14         37 my ($str, $any_vars) = _arg_word_or_string ($count_arg, $document);
91 14 100       114 ($str =~ /^[[:alpha:]_]\w*$/ && ! $any_vars)
92             }) {
93 8   66     63 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         2134 my $args_any_vars = 0;
103 42         82 my %arg_keys;
104 42         105 while (@args) {
105 38         90 my $arg = shift @args;
106 38         97 my ($str, $any_vars) = _arg_word_or_string ($arg, $document);
107 38   100     260 $args_any_vars ||= $any_vars;
108             ### arg: @$arg
109             ### $str
110             ### $any_vars
111 38 100       89 if (! $any_vars) {
112 31         79 $arg_keys{$str} = $arg;
113             }
114 38         114 shift @args; # value part
115             }
116              
117 42         78 my %format_keys;
118             my $format_any_vars;
119              
120 42         88 foreach my $format_arg (@format_args) {
121 59         1450 my ($format_str, $any_vars) = _arg_string ($format_arg, $document);
122 59   66     255 $format_any_vars ||= $any_vars;
123              
124 59         314 while ($format_str =~ /\{(\w+)\}/g) {
125 50         129 my $format_key = $1;
126             ### $format_key
127 50         135 $format_keys{$format_key} = 1;
128              
129 50 100 100     233 if (! $args_any_vars && ! exists $arg_keys{$format_key}) {
130 21   33     159 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       2895 if (! $format_any_vars) {
139 39         115 foreach my $arg_key (keys %arg_keys) {
140 28 100       281 if (! exists $format_keys{$arg_key}) {
141 13         27 my $arg = $arg_keys{$arg_key};
142 13 100 33     97 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         2866 return @violations;
153             }
154              
155             sub _arg_word_or_string {
156 52     52   110 my ($arg, $document) = @_;
157 52 100 66     315 if (@$arg == 1 && $arg->[0]->isa('PPI::Token::Word')) {
158 27         82 return ("$arg->[0]", 0);
159             } else {
160 25         66 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   514 my ($arg, $document) = @_;
171             ### _arg_string() ...
172              
173 190         443 my @elems = @$arg;
174 190         396 my $ret = '';
175 190         377 my $any_vars = 0;
176              
177 190         511 while (@elems) {
178 202         583 my $elem = shift @elems;
179              
180 202 100       1501 if ($elem->isa('PPI::Token::Quote')) {
    100          
    100          
    100          
181 85         314 my $str = $elem->string;
182 85 100 100     1208 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     97 $any_vars ||= _string_any_vars ($str);
187             }
188 85         198 $ret .= $str;
189              
190             } elsif ($elem->isa('PPI::Token::HereDoc')) {
191 4         16 my $str = join('',$elem->heredoc);
192 4 50       32 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     34 $any_vars ||= _string_any_vars ($str);
198             }
199 4         21 $ret .= $str;
200              
201             } elsif ($elem->isa('PPI::Token::Number')) {
202             ### number can work like a constant string ...
203 10         34 $ret .= $elem->content;
204              
205             } elsif ($elem->isa('PPI::Token::Word')) {
206             ### word ...
207 88         239 my $next;
208 88 100 66     357 if ($elem eq '__PACKAGE__') {
    100 66        
    100          
    100          
209 8         143 $ret .= _elem_package_name($elem);
210              
211             } elsif ($elem eq '__LINE__') {
212             ### logical line: $elem->location->[3]
213 8         227 $ret .= $elem->location->[3]; # logical line using any #line directives
214              
215             } elsif ($elem eq '__FILE__') {
216 4         148 my $filename = _elem_logical_filename($elem,$document);
217 4 100       40 if (! defined $filename) {
218 2         5 $filename = 'unknown-filename.pl';
219             }
220             ### $filename
221 4         14 $ret .= $filename;
222              
223             } elsif (($next = $elem->snext_sibling)
224             && $next->isa('PPI::Token::Operator')
225             && $next eq '=>') {
226             ### word quoted by => ...
227 64         5471 $ret .= $elem->content;
228 64         343 last;
229             } else {
230             ### some function call or something ...
231 4         310 return ('', 2);
232             }
233              
234             } else {
235             ### some variable or expression or something ...
236 15         65 return ('', 2);
237             }
238              
239              
240 119 100       510 if (! @elems) { last; }
  107         240  
241 12         31 my $op = shift @elems;
242 12 50 33     70 if (! ($op->isa('PPI::Token::Operator') && $op eq '.')) {
243             # something other than "." concat
244 0         0 return ('', 2);
245             }
246             }
247 171         681 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   5778 my ($str) = @_;
254 42         355 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   25 my ($elem) = @_;
263 8 100       35 if (my $packelem = Perl::Critic::Pulp::Utils::elem_package($elem)) {
264 3 100       16 if (my $name = $packelem->namespace) {
265 1         34 return $name;
266             }
267             }
268 7         134 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   11 my ($elem, $document) = @_;
286             ### _elem_logical_filename(): "$elem"
287              
288 4         8 my $filename;
289             $document->find_first (sub {
290 116     116   1282 my ($doc, $e) = @_;
291             # ### comment: (ref $e)." ".$e->content
292 116 100       239 if ($e == $elem) {
293             ### not found before target elem, stop ...
294 4         24 return undef;
295             }
296 112 100 66     696 if ($e->isa('PPI::Token::Comment')
297             && $e->content =~ $line_directive_re) {
298 2         39 $filename = $3;
299             ### found line directive: $filename
300             }
301 112         267 return 0; # continue
302 4         35 });
303 4 100       65 if (defined $filename) {
304 2         31 return $filename;
305             } else {
306             ### not found, use document: $document->filename
307 2         10 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, 2019 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