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, 2021 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   1410 use 5.006;
  40         285  
21 40     40   409 use strict;
  40         207  
  40         1003  
22 40     40   296 use warnings;
  40         374  
  40         2546  
23              
24 40     40   332 use base 'Perl::Critic::Policy';
  40         134  
  40         5148  
25 40         3286 use Perl::Critic::Utils qw(is_function_call
26             parse_arg_list
27 40     40   185603 interpolate);
  40         208  
28              
29             # uncomment this to run the ### lines
30             # use Smart::Comments;
31              
32             our $VERSION = 99;
33              
34 40     40   278 use constant supported_parameters => ();
  40         100  
  40         2837  
35 40     40   353 use constant default_severity => $Perl::Critic::Utils::SEVERITY_MEDIUM;
  40         107  
  40         2538  
36 40     40   344 use constant default_themes => qw(pulp bugs);
  40         93  
  40         2557  
37 40     40   295 use constant applies_to => 'PPI::Token::Word';
  40         110  
  40         61209  
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 789020 my ($self, $elem, $document) = @_;
48              
49 83         332 my $funcname = $elem->content;
50 83         466 $funcname =~ s/^Locale::TextDomain:://;
51 83 100       441 $funcs{$funcname} || return;
52             ### TextDomainPlaceholders: $elem->content
53              
54 49 100       292 is_function_call($elem) || return;
55              
56 42         14697 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         243 my @args = parse_arg_list ($elem);
74             ### got total arg count: scalar(@args)
75              
76 42 100       8057 if ($funcname =~ /p/) {
77             # msgctxt context arg to __p, __npx
78 4         12 shift @args;
79             }
80              
81             # one format to __x, two to __nx and other "n" funcs
82 42 100       285 my @format_args = splice @args, 0, ($funcname =~ /n/ ? 2 : 1);
83              
84 42 100       202 if ($funcname =~ /n/) {
85             # count arg to __nx and other "n" funcs
86 17         109 my $count_arg = shift @args;
87 17 100 100     117 if (! $count_arg
88             || do {
89             # if it looks like a keyword symbol foo=> or 'foo' etc
90 14         66 my ($str, $any_vars) = _arg_word_or_string ($count_arg, $document);
91 14 100       155 ($str =~ /^[[:alpha:]_]\w*$/ && ! $any_vars)
92             }) {
93 8   66     97 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         2409 my $args_any_vars = 0;
103 42         102 my %arg_keys;
104 42         180 while (@args) {
105 38         199 my $arg = shift @args;
106 38         165 my ($str, $any_vars) = _arg_word_or_string ($arg, $document);
107 38   100     362 $args_any_vars ||= $any_vars;
108             ### arg: @$arg
109             ### $str
110             ### $any_vars
111 38 100       158 if (! $any_vars) {
112 31         105 $arg_keys{$str} = $arg;
113             }
114 38         137 shift @args; # value part
115             }
116              
117 42         134 my %format_keys;
118             my $format_any_vars;
119              
120 42         172 foreach my $format_arg (@format_args) {
121 59         1675 my ($format_str, $any_vars) = _arg_string ($format_arg, $document);
122 59   66     356 $format_any_vars ||= $any_vars;
123              
124 59         423 while ($format_str =~ /\{(\w+)\}/g) {
125 50         221 my $format_key = $1;
126             ### $format_key
127 50         156 $format_keys{$format_key} = 1;
128              
129 50 100 100     348 if (! $args_any_vars && ! exists $arg_keys{$format_key}) {
130 21   33     217 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       3320 if (! $format_any_vars) {
139 39         181 foreach my $arg_key (keys %arg_keys) {
140 28 100       409 if (! exists $format_keys{$arg_key}) {
141 13         46 my $arg = $arg_keys{$arg_key};
142 13 100 33     172 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         3117 return @violations;
153             }
154              
155             sub _arg_word_or_string {
156 52     52   160 my ($arg, $document) = @_;
157 52 100 66     491 if (@$arg == 1 && $arg->[0]->isa('PPI::Token::Word')) {
158 27         129 return ("$arg->[0]", 0);
159             } else {
160 25         101 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   482 my ($arg, $document) = @_;
171             ### _arg_string() ...
172              
173 190         441 my @elems = @$arg;
174 190         378 my $ret = '';
175 190         412 my $any_vars = 0;
176              
177 190         590 while (@elems) {
178 202         593 my $elem = shift @elems;
179              
180 202 100       1297 if ($elem->isa('PPI::Token::Quote')) {
    100          
    100          
    100          
181 85         411 my $str = $elem->string;
182 85 100 100     1489 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     109 $any_vars ||= _string_any_vars ($str);
187             }
188 85         240 $ret .= $str;
189              
190             } elsif ($elem->isa('PPI::Token::HereDoc')) {
191 4         23 my $str = join('',$elem->heredoc);
192 4 50       36 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     51 $any_vars ||= _string_any_vars ($str);
198             }
199 4         29 $ret .= $str;
200              
201             } elsif ($elem->isa('PPI::Token::Number')) {
202             ### number can work like a constant string ...
203 10         38 $ret .= $elem->content;
204              
205             } elsif ($elem->isa('PPI::Token::Word')) {
206             ### word ...
207 88         147 my $next;
208 88 100 66     232 if ($elem eq '__PACKAGE__') {
    100 66        
    100          
    100          
209 8         121 $ret .= _elem_package_name($elem);
210              
211             } elsif ($elem eq '__LINE__') {
212             ### logical line: $elem->location->[3]
213 8         208 $ret .= $elem->location->[3]; # logical line using any #line directives
214              
215             } elsif ($elem eq '__FILE__') {
216 4         133 my $filename = _elem_logical_filename($elem,$document);
217 4 100       32 if (! defined $filename) {
218 2         5 $filename = 'unknown-filename.pl';
219             }
220             ### $filename
221 4         10 $ret .= $filename;
222              
223             } elsif (($next = $elem->snext_sibling)
224             && $next->isa('PPI::Token::Operator')
225             && $next eq '=>') {
226             ### word quoted by => ...
227 64         5049 $ret .= $elem->content;
228 64         293 last;
229             } else {
230             ### some function call or something ...
231 4         284 return ('', 2);
232             }
233              
234             } else {
235             ### some variable or expression or something ...
236 15         73 return ('', 2);
237             }
238              
239              
240 119 100       588 if (! @elems) { last; }
  107         253  
241 12         27 my $op = shift @elems;
242 12 50 33     63 if (! ($op->isa('PPI::Token::Operator') && $op eq '.')) {
243             # something other than "." concat
244 0         0 return ('', 2);
245             }
246             }
247 171         663 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   5875 my ($str) = @_;
254 42         333 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       25 if (my $packelem = Perl::Critic::Pulp::Utils::elem_package($elem)) {
264 3 100       12 if (my $name = $packelem->namespace) {
265 1         33 return $name;
266             }
267             }
268 7         117 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   10 my ($elem, $document) = @_;
286             ### _elem_logical_filename(): "$elem"
287              
288 4         9 my $filename;
289             $document->find_first (sub {
290 116     116   1203 my ($doc, $e) = @_;
291             # ### comment: (ref $e)." ".$e->content
292 116 100       232 if ($e == $elem) {
293             ### not found before target elem, stop ...
294 4         25 return undef;
295             }
296 112 100 66     666 if ($e->isa('PPI::Token::Comment')
297             && $e->content =~ $line_directive_re) {
298 2         33 $filename = $3;
299             ### found line directive: $filename
300             }
301 112         241 return 0; # continue
302 4         33 });
303 4 100       60 if (defined $filename) {
304 2         7 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, 2021 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