use
5.010001;
:booleans :characters :severities hashify precedence_of
split_nodes_on_comma
}
;
our
$VERSION
=
'1.156'
;
Readonly::Scalar
my
$SPLIT
=>
q{split}
;
Readonly::Scalar
my
$WHILE
=>
q{while}
;
Readonly::Hash
my
%ZERO_BASED_CAPTURE_REFERENCE
=>
hashify(
qw< ${^CAPTURE} >
);
Readonly::Hash
my
%CAPTURE_REFERENCE
=> (
hashify(
qw< $+ $- ${^CAPTURE_ALL} >
),
%ZERO_BASED_CAPTURE_REFERENCE
);
Readonly::Hash
my
%CAPTURE_REFERENCE_ENGLISH
=> (
hashify(
qw{ $LAST_PAREN_MATCH $LAST_MATCH_START $LAST_MATCH_END }
),
%CAPTURE_REFERENCE
);
Readonly::Hash
my
%CAPTURE_ARRAY
=> hashify(
qw< @- @+ @{^CAPTURE} >
);
Readonly::Hash
my
%CAPTURE_ARRAY_ENGLISH
=> (
hashify(
qw< @LAST_MATCH_START @LAST_MATCH_END >
),
%CAPTURE_ARRAY
);
Readonly::Hash
my
%CAPTURE_HASH
=> hashify(
qw< %- %+ %{^CAPTURE} >
);
Readonly::Hash
my
%CAPTURE_HASH_ENGLISH
=> (
hashify(
qw< %LAST_PAREN_MATCH >
),
%CAPTURE_HASH
);
Readonly::Scalar
my
$DESC
=>
q{Only use a capturing group if you plan to use the captured value}
;
Readonly::Scalar
my
$EXPL
=> [252];
sub
supported_parameters {
return
qw()
}
sub
default_severity {
return
$SEVERITY_MEDIUM
}
sub
default_themes {
return
qw( core pbp maintenance )
}
sub
applies_to {
return
qw< PPI::Token::Regexp::Match PPI::Token::Regexp::Substitute >
;
}
Readonly::Scalar
my
$NUM_CAPTURES_FOR_GLOBAL
=> 100;
sub
violates {
my
(
$self
,
$elem
,
$doc
) =
@_
;
return
if
0 >
index
$elem
->content(),
'('
;
my
$re
=
$doc
->ppix_regexp_from_element(
$elem
) or
return
;
$re
->failures() and
return
;
my
$ncaptures
=
$re
->max_capture_number() or
return
;
my
@captures
= (
undef
) x
$ncaptures
;
my
%named_captures
;
foreach
my
$struct
( @{
$re
->find(
'PPIx::Regexp::Structure::NamedCapture'
) || [] } ) {
push
@{
$named_captures
{
$struct
->name() } ||= [] },
$struct
->number();
}
return
if
_enough_uses_in_regexp(
$re
, \
@captures
, \
%named_captures
,
$doc
);
if
(
$re
->modifier_asserted(
'g'
)
and not _check_if_in_while_condition_or_block(
$elem
) ) {
$ncaptures
=
$NUM_CAPTURES_FOR_GLOBAL
;
$#captures
=
$ncaptures
- 1;
}
if
( !
%named_captures
) {
return
if
_enough_assignments(
$elem
, \
@captures
);
return
if
_is_in_slurpy_array_context(
$elem
);
}
return
if
_enough_magic(
$elem
,
$re
, \
@captures
, \
%named_captures
,
$doc
);
return
$self
->violation(
$DESC
,
$EXPL
,
$elem
);
}
sub
_enough_uses_in_regexp {
my
(
$re
,
$captures
,
$named_captures
,
$doc
) =
@_
;
foreach
my
$token
( @{
$re
->find(
'PPIx::Regexp::Token::Reference'
)
|| [] } ) {
if
(
$token
->is_named() ) {
_record_named_capture(
$token
->name(),
$captures
,
$named_captures
);
}
else
{
_record_numbered_capture(
$token
->absolute(),
$captures
);
}
}
foreach
my
$token
( @{
$re
->find(
'PPIx::Regexp::Token::Code'
) || [] } ) {
my
$ppi
=
$token
->ppi() or
next
;
_check_node_children(
$ppi
, {
regexp
=>
$re
,
numbered_captures
=>
$captures
,
named_captures
=>
$named_captures
,
document
=>
$doc
,
}, _make_regexp_checker() );
}
return
( none {not
defined
} @{
$captures
} )
&& ( !%{
$named_captures
} ||
none {
defined
}
values
%{
$named_captures
} );
}
sub
_enough_assignments {
my
(
$elem
,
$captures
) =
@_
;
my
$psib
=
$elem
->sprevious_sibling;
SIBLING:
while
(1) {
return
if
!
$psib
;
if
(
$psib
->isa(
'PPI::Token::Operator'
)) {
last
SIBLING
if
q{=}
eq
$psib
->content;
return
if
q{!~}
eq
$psib
->content;
}
$psib
=
$psib
->sprevious_sibling;
}
$psib
=
$psib
->sprevious_sibling;
return
if
!
$psib
;
if
(
$psib
->isa(
'PPI::Token::Symbol'
)) {
return
$TRUE
if
_symbol_is_slurpy(
$psib
);
}
elsif
(
$psib
->isa(
'PPI::Structure::Block'
)) {
return
$TRUE
if
_is_preceded_by_array_or_hash_cast(
$psib
);
}
elsif
(
$psib
->isa(
'PPI::Structure::List'
)) {
my
@args
=
$psib
->schildren;
return
$TRUE
if
not
@args
;
if
( 1 ==
@args
&&
$args
[0]->isa(
'PPI::Statement::Expression'
) ) {
@args
=
$args
[0]->schildren;
}
my
@parts
= split_nodes_on_comma(
@args
);
PART:
for
my
$i
(0 ..
$#parts
) {
if
(1 == @{
$parts
[
$i
]}) {
my
$var
=
$parts
[
$i
]->[0];
if
(
$var
->isa(
'PPI::Token::Symbol'
) ||
$var
->isa(
'PPI::Token::Cast'
)) {
return
$TRUE
if
_has_array_sigil(
$var
);
}
}
_record_numbered_capture(
$i
+ 1,
$captures
);
}
}
return
none {not
defined
} @{
$captures
};
}
sub
_symbol_is_slurpy {
my
(
$symbol
) =
@_
;
return
$TRUE
if
_has_array_sigil(
$symbol
);
return
$TRUE
if
_has_hash_sigil(
$symbol
);
return
$TRUE
if
_is_preceded_by_array_or_hash_cast(
$symbol
);
return
;
}
sub
_has_array_sigil {
my
(
$elem
) =
@_
;
return
q{@}
eq
substr
$elem
->content, 0, 1;
}
sub
_has_hash_sigil {
my
(
$elem
) =
@_
;
return
q{%}
eq
substr
$elem
->content, 0, 1;
}
sub
_is_preceded_by_array_or_hash_cast {
my
(
$elem
) =
@_
;
my
$psib
=
$elem
->sprevious_sibling;
my
$cast
;
while
(
$psib
&&
$psib
->isa(
'PPI::Token::Cast'
)) {
$cast
=
$psib
;
$psib
=
$psib
->sprevious_sibling;
}
return
if
!
$cast
;
my
$sigil
=
substr
$cast
->content, 0, 1;
return
q{@}
eq
$sigil
||
q{%}
eq
$sigil
;
}
sub
_is_in_slurpy_array_context {
my
(
$elem
) =
@_
;
my
$psib
=
$elem
->sprevious_sibling;
if
(
$psib
&&
$psib
->content eq
q{=~}
) {
$psib
= _skip_lhs(
$psib
);
}
if
(!
$psib
) {
my
$parent
=
$elem
->parent;
return
if
!
$parent
;
if
(
$parent
->isa(
'PPI::Statement'
)) {
$parent
=
$parent
->parent;
return
if
!
$parent
;
}
if
(
$parent
->isa(
'PPI::Structure::List'
) ) {
my
$parent_statement
=
$parent
->statement() or
return
$TRUE
;
return
$TRUE
if
not
$parent_statement
->isa(
'PPI::Statement::Compound'
);
return
$TRUE
if
$parent_statement
->type() ne
'foreach'
;
}
return
$TRUE
if
$parent
->isa(
'PPI::Structure::Constructor'
);
if
(
$parent
->isa(
'PPI::Structure::Block'
)) {
return
$TRUE
if
refaddr(
$elem
->statement)
eq refaddr([
$parent
->schildren]->[-1]);
}
return
;
}
if
(
$psib
->isa(
'PPI::Token::Operator'
)) {
return
q{,}
eq
$psib
->content;
}
return
$TRUE
;
}
sub
_skip_lhs {
my
(
$elem
) =
@_
;
$elem
=
$elem
->sprevious_sibling();
return
$elem
;
}
sub
_enough_magic {
my
(
$elem
,
$re
,
$captures
,
$named_captures
,
$doc
) =
@_
;
_check_for_magic(
$elem
,
$re
,
$captures
,
$named_captures
,
$doc
);
return
( none {not
defined
} @{
$captures
} )
&& ( !%{
$named_captures
} ||
none {
defined
}
values
%{
$named_captures
} );
}
sub
_check_for_magic {
my
(
$elem
,
$re
,
$captures
,
$named_captures
,
$doc
) =
@_
;
my
$arg
= {
regexp
=>
$re
,
numbered_captures
=>
$captures
,
named_captures
=>
$named_captures
,
document
=>
$doc
,
};
if
(
my
$prior_token
=
$elem
->sprevious_sibling() ) {
$arg
->{negated} =
$prior_token
->isa(
'PPI::Token::Operator'
) &&
q<!~>
eq
$prior_token
->content();
}
return
if
! _check_rest_of_statement(
$elem
,
$arg
);
my
$parent
=
$elem
->parent();
while
(
$parent
&& !
$parent
->isa(
'PPI::Statement::Sub'
)) {
return
if
! _check_rest_of_statement(
$parent
,
$arg
);
$parent
=
$parent
->parent();
}
return
;
}
sub
_check_if_in_while_condition_or_block {
my
(
$elem
) =
@_
;
$elem
or
return
;
my
$parent
=
$elem
->parent() or
return
;
$parent
->isa(
'PPI::Statement'
) or
return
;
my
$item
=
$parent
=
$parent
->parent() or
return
;
if
(
$item
->isa(
'PPI::Structure::Block'
) ) {
$item
=
$item
->sprevious_sibling() or
return
;
}
$item
->isa(
'PPI::Structure::Condition'
) or
return
;
$item
=
$item
->sprevious_sibling() or
return
;
$item
->isa(
'PPI::Token::Word'
) or
return
;
return
$WHILE
eq
$item
->content();
}
{
Readonly::Hash
my
%SHORTCUT_OPERATOR
=> (
q<||>
=>
$FALSE
,
q<//>
=>
$FALSE
,
and
=>
$TRUE
,
or
=>
$FALSE
,
);
sub
_make_regexp_checker {
my
(
$parent
) =
@_
;
$parent
and not
$parent
->()
and
return
sub
{
return
$FALSE
};
my
$check
=
$TRUE
;
my
$precedence
= 0;
return
sub
{
my
(
$elem
) =
@_
;
$elem
or
return
$check
;
if
(
$elem
->isa(
'PPI::Token::Regexp'
) ) {
return
_regexp_is_in_split(
$elem
) ?
$FALSE
:
$check
;
}
if
(
$elem
->isa(
'PPI::Token::Structure'
)
&&
q<;>
eq
$elem
->content() ) {
$check
=
$TRUE
;
$precedence
= 0;
return
$FALSE
;
}
$elem
->isa(
'PPI::Token::Operator'
)
or
return
$FALSE
;
my
$content
=
$elem
->content();
defined
(
my
$oper_check
=
$SHORTCUT_OPERATOR
{
$content
} )
or
return
$FALSE
;
my
$oper_precedence
= precedence_of(
$content
);
$oper_precedence
>=
$precedence
or
return
$FALSE
;
$precedence
=
$oper_precedence
;
$check
=
$oper_check
;
return
$FALSE
;
};
}
}
sub
_regexp_is_in_split {
my
(
$elem
) =
@_
;
my
$prev
;
if
( ! (
$prev
=
$elem
->sprevious_sibling() ) ) {
my
$stmt
=
$elem
->statement()
or
return
$FALSE
;
$stmt
->parent()
or
return
$FALSE
;
$prev
=
$elem
->sprevious_sibling()
or
return
$FALSE
;
}
return
$prev
->isa(
'PPI::Token::Word'
) &&
$SPLIT
eq
$prev
->content();
}
sub
_check_rest_of_statement {
my
(
$elem
,
$arg
) =
@_
;
my
$checker
= _make_regexp_checker();
my
$nsib
=
$elem
->snext_sibling;
if
(
$arg
->{negated} && _is_condition_of_if_statement(
$elem
) ) {
while
(
$nsib
&& !
$nsib
->isa(
'PPI::Structure::Block'
) ) {
$nsib
=
$nsib
->snext_sibling();
}
$nsib
and
$nsib
=
$nsib
->snext_sibling();
}
while
(
$nsib
) {
return
if
$checker
->(
$nsib
);
if
(
$nsib
->isa(
'PPI::Node'
)) {
return
if
! _check_node_children(
$nsib
,
$arg
,
$checker
);
}
else
{
_mark_magic(
$nsib
,
$arg
->{regexp},
$arg
->{numbered_captures},
$arg
->{named_captures},
$arg
->{document} );
}
$nsib
=
$nsib
->snext_sibling;
}
return
$TRUE
;
}
{
Readonly::Hash
my
%IS_IF_STATEMENT
=> hashify(
qw{ if elsif }
);
sub
_is_condition_of_if_statement {
my
(
$elem
) =
@_
;
$elem
and
$elem
->isa(
'PPI::Structure::Condition'
)
or
return
$FALSE
;
my
$psib
=
$elem
->sprevious_sibling()
or
return
$FALSE
;
$psib
->isa(
'PPI::Token::Word'
)
or
return
$FALSE
;
return
$IS_IF_STATEMENT
{
$psib
->content() };
}
}
sub
_check_node_children {
my
(
$elem
,
$arg
,
$parent_checker
) =
@_
;
my
$checker
= _make_regexp_checker(
$parent_checker
);
for
my
$child
(
$elem
->schildren) {
return
if
$checker
->(
$child
);
if
(
$child
->isa(
'PPI::Node'
)) {
return
if
! _check_node_children(
$child
,
$arg
,
$checker
);
}
else
{
_mark_magic(
$child
,
$arg
->{regexp},
$arg
->{numbered_captures},
$arg
->{named_captures},
$arg
->{document});
}
}
return
$TRUE
;
}
sub
_mark_magic {
my
(
$elem
,
$re
,
$captures
,
$named_captures
,
$doc
) =
@_
;
if
( _is_double_quotish_element(
$elem
) ) {
_mark_magic_in_content(
$elem
->content(),
$re
,
$captures
,
$named_captures
,
$doc
);
return
;
}
if
(
$elem
->isa(
'PPI::Token::HereDoc'
) ) {
$elem
->content() =~ m/ \A << ~? \s* ' /sxm
or _mark_magic_in_content(
join
(
$EMPTY
,
$elem
->heredoc() ),
$re
,
$captures
,
$named_captures
,
$doc
);
return
;
}
my
$content
=
$elem
->content();
my
(
$capture_ref
,
$capture_array
,
$capture_hash
) =
$doc
->uses_module(
'English'
) ?
( \
%CAPTURE_REFERENCE_ENGLISH
, \
%CAPTURE_ARRAY_ENGLISH
, \
%CAPTURE_HASH_ENGLISH
) :
( \
%CAPTURE_REFERENCE
, \
%CAPTURE_ARRAY
, \
%CAPTURE_HASH
);
$elem
->isa(
'PPI::Token::Magic'
)
or
$capture_ref
->{
$content
}
or
$capture_array
->{
$content
}
or
$capture_hash
->{
$content
}
or
return
;
if
(
$content
=~ m/ \A \$ ( \d+ ) /xms ) {
my
$num
= $1;
if
(0 <
$num
) {
if
(
$num
<= @{
$captures
}) {
_record_numbered_capture(
$num
,
$captures
);
}
}
}
elsif
(
$capture_array
->{
$content
} ) {
foreach
my
$num
( 1 .. @{
$captures
} ) {
_record_numbered_capture(
$num
,
$captures
);
}
}
elsif
(
$capture_hash
->{
$content
} ) {
foreach
my
$name
(
keys
%{
$named_captures
} ) {
_record_named_capture(
$name
,
$captures
,
$named_captures
);
}
}
elsif
(
$capture_ref
->{
$content
} ) {
_mark_magic_subscripted_code(
$elem
,
$re
,
$captures
,
$named_captures
);
}
return
;
}
sub
_mark_magic_subscripted_code {
my
(
$elem
,
$re
,
$captures
,
$named_captures
) =
@_
;
my
$subscr
=
$elem
->snext_sibling() or
return
;
$subscr
->isa(
'PPI::Structure::Subscript'
) or
return
;
my
$subval
=
$subscr
->content();
_record_subscripted_capture(
$elem
->content(),
$subval
,
$re
,
$captures
,
$named_captures
);
return
;
}
sub
_mark_magic_in_content {
my
(
$content
,
$re
,
$captures
,
$named_captures
,
$doc
) =
@_
;
my
(
$capture_ref
,
$capture_array
) =
$doc
->uses_module(
'English'
) ?
( \
%CAPTURE_REFERENCE_ENGLISH
, \
%CAPTURE_ARRAY_ENGLISH
) :
( \
%CAPTURE_REFERENCE
, \
%CAPTURE_ARRAY
);
while
(
$content
=~ m< ( [\$\@] (?:
[{] \^? (?: \w+ | . ) [}] | \w+ | . ) ) >sxmg ) {
my
$name
= $1;
$name
=~ s/ \A ( [\$\@] ) [{] (?! \^ ) /$1/sxm
and
$name
=~ s/ [}] \z //sxm;
if
(
$name
=~ m/ \A \$ ( \d+ ) \z /sxm ) {
my
$num
= $1;
0 <
$num
and
$num
<= @{
$captures
}
and _record_numbered_capture(
$num
,
$captures
);
}
elsif
(
$capture_array
->{
$name
} ) {
foreach
my
$num
( 1 .. @{
$captures
} ) {
_record_numbered_capture(
$num
,
$captures
);
}
}
elsif
(
$capture_ref
->{
$name
} &&
$content
=~ m/ \G ( [{] [^}]+ [}] | [[] [^]] []] ) /smxgc )
{
_record_subscripted_capture(
$name
, $1,
$re
,
$captures
,
$named_captures
);
}
}
return
;
}
sub
_is_double_quotish_element {
my
(
$elem
) =
@_
;
$elem
or
return
;
my
$content
=
$elem
->content();
if
(
$elem
->isa(
'PPI::Token::QuoteLike::Command'
) ) {
return
$content
!~ m/ \A
qx \s*
' /sxm;
}
foreach
my
$class
(
qw{
PPI::Token::Quote::Double
PPI::Token::Quote::Interpolate
PPI::Token::QuoteLike::Backtick
PPI::Token::QuoteLike::Readline
}
) {
$elem
->isa(
$class
) and
return
$TRUE
;
}
return
$FALSE
;
}
sub
_record_subscripted_capture {
my
(
$variable_name
,
$suffix
,
$re
,
$captures
,
$named_captures
) =
@_
;
if
(
$suffix
=~ m/ \A [{] ( .*? ) [}] /smx ) {
(
my
$name
= $1 ) =~ s/ \A ( ["'] ) ( .*? ) \1 \z /$2/smx;
_record_named_capture(
$name
,
$captures
,
$named_captures
);
}
elsif
(
$suffix
=~ m/ \A [[] \s* ( [-+]? \d+ ) \s* []] /smx ) {
my
$num
= $1 + 0;
$num
>= 0
and
$ZERO_BASED_CAPTURE_REFERENCE
{
$variable_name
}
and
$num
++;
_record_numbered_capture(
$num
,
$captures
,
$re
);
}
return
;
}
sub
_record_named_capture {
my
(
$name
,
$captures
,
$named_captures
) =
@_
;
defined
(
my
$numbers
=
$named_captures
->{
$name
} ) or
return
;
foreach
my
$capnum
( @{
$numbers
} ) {
_record_numbered_capture(
$capnum
,
$captures
);
}
$named_captures
->{
$name
} =
undef
;
return
;
}
sub
_record_numbered_capture {
my
(
$number
,
$captures
,
$re
) =
@_
;
$re
and
$number
< 0
and
$number
=
$re
->max_capture_number() +
$number
+ 1;
return
if
$number
<= 0;
$captures
->[
$number
- 1 ] = 1;
return
;
}
1;