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   295319 use 5.006;
  40         175  
21 40     40   325 use strict;
  40         93  
  40         1302  
22 40     40   326 use warnings;
  40         161  
  40         2649  
23              
24 40     40   301 use base 'Perl::Critic::Policy';
  40         230  
  40         6564  
25 40         4186 use Perl::Critic::Utils qw(is_function_call
26             parse_arg_list
27 40     40   183917 interpolate);
  40         88  
28              
29             # uncomment this to run the ### lines
30             # use Smart::Comments;
31              
32             our $VERSION = 100;
33              
34 40     40   283 use constant supported_parameters => ();
  40         130  
  40         3355  
35 40     40   283 use constant default_severity => $Perl::Critic::Utils::SEVERITY_MEDIUM;
  40         123  
  40         2959  
36 40     40   303 use constant default_themes => qw(pulp bugs);
  40         124  
  40         2975  
37 40     40   260 use constant applies_to => 'PPI::Token::Word';
  40         120  
  40         82303  
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 1120498 my ($self, $elem, $document) = @_;
48              
49 83         491 my $funcname = $elem->content;
50 83         427 $funcname =~ s/^Locale::TextDomain:://;
51 83 100       340 $funcs{$funcname} || return;
52             ### TextDomainPlaceholders: $elem->content
53              
54 49 100       177 is_function_call($elem) || return;
55              
56 42         11994 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         143 my @args = parse_arg_list ($elem);
74             ### got total arg count: scalar(@args)
75              
76 42 100       7163 if ($funcname =~ /p/) {
77             # msgctxt context arg to __p, __npx
78 4         9 shift @args;
79             }
80              
81             # one format to __x, two to __nx and other "n" funcs
82 42 100       212 my @format_args = splice @args, 0, ($funcname =~ /n/ ? 2 : 1);
83              
84 42 100       145 if ($funcname =~ /n/) {
85             # count arg to __nx and other "n" funcs
86 17         40 my $count_arg = shift @args;
87 17 100 100     66 if (! $count_arg
88             || do {
89             # if it looks like a keyword symbol foo=> or 'foo' etc
90 14         44 my ($str, $any_vars) = _arg_word_or_string ($count_arg, $document);
91 14 100       143 ($str =~ /^[[:alpha:]_]\w*$/ && ! $any_vars)
92             }) {
93 8   66     89 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         2176 my $args_any_vars = 0;
103 42         85 my %arg_keys;
104 42         139 while (@args) {
105 38         77 my $arg = shift @args;
106 38         108 my ($str, $any_vars) = _arg_word_or_string ($arg, $document);
107 38   100     286 $args_any_vars ||= $any_vars;
108             ### arg: @$arg
109             ### $str
110             ### $any_vars
111 38 100       86 if (! $any_vars) {
112 31         89 $arg_keys{$str} = $arg;
113             }
114 38         119 shift @args; # value part
115             }
116              
117 42         117 my %format_keys;
118             my $format_any_vars;
119              
120 42         95 foreach my $format_arg (@format_args) {
121 59         1780 my ($format_str, $any_vars) = _arg_string ($format_arg, $document);
122 59   66     296 $format_any_vars ||= $any_vars;
123              
124 59         463 while ($format_str =~ /\{(\w+)\}/g) {
125 50         168 my $format_key = $1;
126             ### $format_key
127 50         170 $format_keys{$format_key} = 1;
128              
129 50 100 100     316 if (! $args_any_vars && ! exists $arg_keys{$format_key}) {
130 21   33     179 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       3522 if (! $format_any_vars) {
139 39         115 foreach my $arg_key (keys %arg_keys) {
140 28 100       349 if (! exists $format_keys{$arg_key}) {
141 13         34 my $arg = $arg_keys{$arg_key};
142 13 100 33     173 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         3106 return @violations;
153             }
154              
155             sub _arg_word_or_string {
156 52     52   136 my ($arg, $document) = @_;
157 52 100 66     373 if (@$arg == 1 && $arg->[0]->isa('PPI::Token::Word')) {
158 27         97 return ("$arg->[0]", 0);
159             } else {
160 25         78 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   499 my ($arg, $document) = @_;
171             ### _arg_string() ...
172              
173 190         460 my @elems = @$arg;
174 190         490 my $ret = '';
175 190         375 my $any_vars = 0;
176              
177 190         519 while (@elems) {
178 202         666 my $elem = shift @elems;
179              
180 202 100       1587 if ($elem->isa('PPI::Token::Quote')) {
    100          
    100          
    100          
181 85         395 my $str = $elem->string;
182 85 100 100     1296 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     94 $any_vars ||= _string_any_vars ($str);
187             }
188 85         179 $ret .= $str;
189              
190             } elsif ($elem->isa('PPI::Token::HereDoc')) {
191 4         16 my $str = join('',$elem->heredoc);
192 4 50       49 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         19 $ret .= $str;
200              
201             } elsif ($elem->isa('PPI::Token::Number')) {
202             ### number can work like a constant string ...
203 10         42 $ret .= $elem->content;
204              
205             } elsif ($elem->isa('PPI::Token::Word')) {
206             ### word ...
207 88         203 my $next;
208 88 100 66     371 if ($elem eq '__PACKAGE__') {
    100 66        
    100          
    100          
209 8         199 $ret .= _elem_package_name($elem);
210              
211             } elsif ($elem eq '__LINE__') {
212             ### logical line: $elem->location->[3]
213 8         294 $ret .= $elem->location->[3]; # logical line using any #line directives
214              
215             } elsif ($elem eq '__FILE__') {
216 4         203 my $filename = _elem_logical_filename($elem,$document);
217 4 100       47 if (! defined $filename) {
218 2         5 $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         5916 $ret .= $elem->content;
228 64         326 last;
229             } else {
230             ### some function call or something ...
231 4         385 return ('', 2);
232             }
233              
234             } else {
235             ### some variable or expression or something ...
236 15         70 return ('', 2);
237             }
238              
239              
240 119 100       529 if (! @elems) { last; }
  107         263  
241 12         29 my $op = shift @elems;
242 12 50 33     94 if (! ($op->isa('PPI::Token::Operator') && $op eq '.')) {
243             # something other than "." concat
244 0         0 return ('', 2);
245             }
246             }
247 171         898 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   8933 my ($str) = @_;
254 42         336 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       34 if (my $packelem = Perl::Critic::Pulp::Utils::elem_package($elem)) {
264 3 100       18 if (my $name = $packelem->namespace) {
265 1         42 return $name;
266             }
267             }
268 7         173 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   14 my ($elem, $document) = @_;
286             ### _elem_logical_filename(): "$elem"
287              
288 4         10 my $filename;
289             $document->find_first (sub {
290 116     116   1509 my ($doc, $e) = @_;
291             # ### comment: (ref $e)." ".$e->content
292 116 100       361 if ($e == $elem) {
293             ### not found before target elem, stop ...
294 4         29 return undef;
295             }
296 112 100 66     865 if ($e->isa('PPI::Token::Comment')
297             && $e->content =~ $line_directive_re) {
298 2         48 $filename = $3;
299             ### found line directive: $filename
300             }
301 112         330 return 0; # continue
302 4         49 });
303 4 100       86 if (defined $filename) {
304 2         10 return $filename;
305             } else {
306             ### not found, use document: $document->filename
307 2         13 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