Perl-Critic-Policy-Variables-ProhibitLoopOnHash-0.009000755001750001750 015131133604 22517 5ustar00sawyersawyer000000000000README100644001750001750 54415131133604 23443 0ustar00sawyersawyer000000000000Perl-Critic-Policy-Variables-ProhibitLoopOnHash-0.009This archive contains the distribution Perl-Critic-Policy-Variables-ProhibitLoopOnHash, version 0.009: Don't write loops on hashes, only on keys and values of hashes This software is Copyright (c) 2026 by Sawyer X. This is free software, licensed under: The MIT (X11) License This README file was generated by Dist::Zilla::Plugin::Readme v6.036. Changes100644001750001750 234115131133604 24073 0ustar00sawyersawyer000000000000Perl-Critic-Policy-Variables-ProhibitLoopOnHash-0.0090.009 2026-01-12 09:57:39+01:00 Europe/Amsterdam * Don't crash on `for my $foo @bar {...}`. (Graham @haarg Knop) * License is now MIT. 0.008 2019-04-22 16:44:54+03:00 Asia/Jerusalem * GH #3: Some typo fixed. (Jakub Wilk) 0.007 2018-08-17 08:32:29+03:00 Asia/Jerusalem * Fix for reference aliasing syntax. (foreach \my %hash...) Thanks, Ahmed Abdrabo! * Fix edge case of empty for with function. (for Class->method($foo);) * Made slightly faster by avoiding an isa() call. Thanks, Jader Dias! 0.006 2018-07-13 15:48:29+03:00 Asia/Jerusalem * Fix for variable attributes (my Dog $pet (%foo)). Thanks, Maciej Sokolowski! 0.005 2018-05-31 11:59:25+03:00 Asia/Jerusalem * RT#125437: Fix "say for @array", reported by Ahmed Abdrabo and Andy (@petdance) Lester. 0.004 2018-05-28 12:19:40+03:00 Asia/Jerusalem * Fix handling of $hash{for} and $hashref->{for}. Thanks, Igor Yamolov! 0.003 2018-03-12 18:33:08+01:00 Europe/Amsterdam * GH #1: Fix example in POD. 0.002 2018-03-12 09:19:59+01:00 Europe/Amsterdam * Fix false positives on map/grep. 0.001 2018-03-08 16:40:44+01:00 Europe/Amsterdam * Blame Ruud H.G. van Tol. LICENSE100644001750001750 220315131133604 23602 0ustar00sawyersawyer000000000000Perl-Critic-Policy-Variables-ProhibitLoopOnHash-0.009This software is Copyright (c) 2026 by Sawyer X. This is free software, licensed under: The MIT (X11) License The MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. cpanfile100644001750001750 15315131133604 24263 0ustar00sawyersawyer000000000000Perl-Critic-Policy-Variables-ProhibitLoopOnHash-0.009requires 'Carp'; requires 'parent'; requires 'List::Util' => '1.33'; requires 'Perl::Critic' => '1.126'; META.yml100644001750001750 171015131133604 24050 0ustar00sawyersawyer000000000000Perl-Critic-Policy-Variables-ProhibitLoopOnHash-0.009--- abstract: "Don't write loops on hashes, only on keys and values of hashes" author: - 'Sawyer X' build_requires: ExtUtils::MakeMaker: '0' File::Spec: '0' Test::More: '0' configure_requires: ExtUtils::MakeMaker: '0' dynamic_config: 0 generated_by: 'Dist::Zilla version 6.036, CPAN::Meta::Converter version 2.150010' license: mit meta-spec: url: http://module-build.sourceforge.net/META-spec-v1.4.html version: '1.4' name: Perl-Critic-Policy-Variables-ProhibitLoopOnHash provides: Perl::Critic::Policy::Variables::ProhibitLoopOnHash: file: lib/Perl/Critic/Policy/Variables/ProhibitLoopOnHash.pm version: '0.009' requires: Carp: '0' List::Util: '1.33' Perl::Critic: '1.126' parent: '0' resources: repository: git://github.com/xsawyerx/perl-critic-policy-variables-prohibitlooponhash.git version: '0.009' x_authority: cpan:XSAWYERX x_generated_by_perl: v5.34.0 x_serialization_backend: 'YAML::Tiny version 1.73' x_spdx_expression: MIT MANIFEST100644001750001750 52615131133604 23714 0ustar00sawyersawyer000000000000Perl-Critic-Policy-Variables-ProhibitLoopOnHash-0.009# This file was automatically generated by Dist::Zilla::Plugin::Manifest v6.036. Changes LICENSE MANIFEST META.json META.yml Makefile.PL README cpanfile lib/Perl/Critic/Policy/Variables/ProhibitLoopOnHash.pm t/00-report-prereqs.dd t/00-report-prereqs.t t/Variables/ProhibitLoopOnHash.run t/author-no-tabs.t t/author-pod-syntax.t t/policies.t META.json100644001750001750 352115131133604 24222 0ustar00sawyersawyer000000000000Perl-Critic-Policy-Variables-ProhibitLoopOnHash-0.009{ "abstract" : "Don't write loops on hashes, only on keys and values of hashes", "author" : [ "Sawyer X" ], "dynamic_config" : 0, "generated_by" : "Dist::Zilla version 6.036, CPAN::Meta::Converter version 2.150010", "license" : [ "mit" ], "meta-spec" : { "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", "version" : 2 }, "name" : "Perl-Critic-Policy-Variables-ProhibitLoopOnHash", "prereqs" : { "configure" : { "requires" : { "ExtUtils::MakeMaker" : "0" } }, "develop" : { "requires" : { "Test::More" : "0.88", "Test::NoTabs" : "0", "Test::Pod" : "1.41" } }, "runtime" : { "requires" : { "Carp" : "0", "List::Util" : "1.33", "Perl::Critic" : "1.126", "parent" : "0" } }, "test" : { "recommends" : { "CPAN::Meta" : "2.120900" }, "requires" : { "ExtUtils::MakeMaker" : "0", "File::Spec" : "0", "Test::More" : "0" } } }, "provides" : { "Perl::Critic::Policy::Variables::ProhibitLoopOnHash" : { "file" : "lib/Perl/Critic/Policy/Variables/ProhibitLoopOnHash.pm", "version" : "0.009" } }, "release_status" : "stable", "resources" : { "repository" : { "type" : "git", "url" : "git://github.com/xsawyerx/perl-critic-policy-variables-prohibitlooponhash.git", "web" : "https://github.com/xsawyerx/perl-critic-policy-variables-prohibitlooponhash" } }, "version" : "0.009", "x_authority" : "cpan:XSAWYERX", "x_generated_by_perl" : "v5.34.0", "x_serialization_backend" : "Cpanel::JSON::XS version 4.27", "x_spdx_expression" : "MIT" } Makefile.PL100644001750001750 244015131133604 24552 0ustar00sawyersawyer000000000000Perl-Critic-Policy-Variables-ProhibitLoopOnHash-0.009# This file was automatically generated by Dist::Zilla::Plugin::MakeMaker v6.036. use strict; use warnings; use ExtUtils::MakeMaker; my %WriteMakefileArgs = ( "ABSTRACT" => "Don't write loops on hashes, only on keys and values of hashes", "AUTHOR" => "Sawyer X", "CONFIGURE_REQUIRES" => { "ExtUtils::MakeMaker" => 0 }, "DISTNAME" => "Perl-Critic-Policy-Variables-ProhibitLoopOnHash", "LICENSE" => "mit", "NAME" => "Perl::Critic::Policy::Variables::ProhibitLoopOnHash", "PREREQ_PM" => { "Carp" => 0, "List::Util" => "1.33", "Perl::Critic" => "1.126", "parent" => 0 }, "TEST_REQUIRES" => { "ExtUtils::MakeMaker" => 0, "File::Spec" => 0, "Test::More" => 0 }, "VERSION" => "0.009", "test" => { "TESTS" => "t/*.t" } ); my %FallbackPrereqs = ( "Carp" => 0, "ExtUtils::MakeMaker" => 0, "File::Spec" => 0, "List::Util" => "1.33", "Perl::Critic" => "1.126", "Test::More" => 0, "parent" => 0 ); unless ( eval { ExtUtils::MakeMaker->VERSION(6.63_03) } ) { delete $WriteMakefileArgs{TEST_REQUIRES}; delete $WriteMakefileArgs{BUILD_REQUIRES}; $WriteMakefileArgs{PREREQ_PM} = \%FallbackPrereqs; } delete $WriteMakefileArgs{CONFIGURE_REQUIRES} unless eval { ExtUtils::MakeMaker->VERSION(6.52) }; WriteMakefile(%WriteMakefileArgs); t000755001750001750 015131133604 22703 5ustar00sawyersawyer000000000000Perl-Critic-Policy-Variables-ProhibitLoopOnHash-0.009policies.t100644001750001750 13615131133604 25017 0ustar00sawyersawyer000000000000Perl-Critic-Policy-Variables-ProhibitLoopOnHash-0.009/tuse strict; use warnings; use Test::Perl::Critic::Policy 'all_policies_ok'; all_policies_ok; author-no-tabs.t100644001750001750 106715131133604 26077 0ustar00sawyersawyer000000000000Perl-Critic-Policy-Variables-ProhibitLoopOnHash-0.009/t BEGIN { unless ($ENV{AUTHOR_TESTING}) { print qq{1..0 # SKIP these tests are for testing by the author\n}; exit } } use strict; use warnings; # this test was generated with Dist::Zilla::Plugin::Test::NoTabs 0.15 use Test::More 0.88; use Test::NoTabs; my @files = ( 'lib/Perl/Critic/Policy/Variables/ProhibitLoopOnHash.pm', 't/00-report-prereqs.dd', 't/00-report-prereqs.t', 't/Variables/ProhibitLoopOnHash.run', 't/author-no-tabs.t', 't/author-pod-syntax.t', 't/policies.t' ); notabs_ok($_) foreach @files; done_testing; author-pod-syntax.t100644001750001750 45415131133604 26621 0ustar00sawyersawyer000000000000Perl-Critic-Policy-Variables-ProhibitLoopOnHash-0.009/t#!perl BEGIN { unless ($ENV{AUTHOR_TESTING}) { print qq{1..0 # SKIP these tests are for testing by the author\n}; exit } } # This file was automatically generated by Dist::Zilla::Plugin::PodSyntaxTests. use strict; use warnings; use Test::More; use Test::Pod 1.41; all_pod_files_ok(); 00-report-prereqs.t100644001750001750 1360115131133604 26460 0ustar00sawyersawyer000000000000Perl-Critic-Policy-Variables-ProhibitLoopOnHash-0.009/t#!perl use strict; use warnings; # This test was generated by Dist::Zilla::Plugin::Test::ReportPrereqs 0.029 use Test::More tests => 1; use ExtUtils::MakeMaker; use File::Spec; # from $version::LAX my $lax_version_re = qr/(?: undef | (?: (?:[0-9]+) (?: \. | (?:\.[0-9]+) (?:_[0-9]+)? )? | (?:\.[0-9]+) (?:_[0-9]+)? ) | (?: v (?:[0-9]+) (?: (?:\.[0-9]+)+ (?:_[0-9]+)? )? | (?:[0-9]+)? (?:\.[0-9]+){2,} (?:_[0-9]+)? ) )/x; # hide optional CPAN::Meta modules from prereq scanner # and check if they are available my $cpan_meta = "CPAN::Meta"; my $cpan_meta_pre = "CPAN::Meta::Prereqs"; my $HAS_CPAN_META = eval "require $cpan_meta; $cpan_meta->VERSION('2.120900')" && eval "require $cpan_meta_pre"; ## no critic # Verify requirements? my $DO_VERIFY_PREREQS = 1; sub _max { my $max = shift; $max = ( $_ > $max ) ? $_ : $max for @_; return $max; } sub _merge_prereqs { my ($collector, $prereqs) = @_; # CPAN::Meta::Prereqs object if (ref $collector eq $cpan_meta_pre) { return $collector->with_merged_prereqs( CPAN::Meta::Prereqs->new( $prereqs ) ); } # Raw hashrefs for my $phase ( keys %$prereqs ) { for my $type ( keys %{ $prereqs->{$phase} } ) { for my $module ( keys %{ $prereqs->{$phase}{$type} } ) { $collector->{$phase}{$type}{$module} = $prereqs->{$phase}{$type}{$module}; } } } return $collector; } my @include = qw( ); my @exclude = qw( ); # Add static prereqs to the included modules list my $static_prereqs = do './t/00-report-prereqs.dd'; # Merge all prereqs (either with ::Prereqs or a hashref) my $full_prereqs = _merge_prereqs( ( $HAS_CPAN_META ? $cpan_meta_pre->new : {} ), $static_prereqs ); # Add dynamic prereqs to the included modules list (if we can) my ($source) = grep { -f } 'MYMETA.json', 'MYMETA.yml'; my $cpan_meta_error; if ( $source && $HAS_CPAN_META && (my $meta = eval { CPAN::Meta->load_file($source) } ) ) { $full_prereqs = _merge_prereqs($full_prereqs, $meta->prereqs); } else { $cpan_meta_error = $@; # capture error from CPAN::Meta->load_file($source) $source = 'static metadata'; } my @full_reports; my @dep_errors; my $req_hash = $HAS_CPAN_META ? $full_prereqs->as_string_hash : $full_prereqs; # Add static includes into a fake section for my $mod (@include) { $req_hash->{other}{modules}{$mod} = 0; } for my $phase ( qw(configure build test runtime develop other) ) { next unless $req_hash->{$phase}; next if ($phase eq 'develop' and not $ENV{AUTHOR_TESTING}); for my $type ( qw(requires recommends suggests conflicts modules) ) { next unless $req_hash->{$phase}{$type}; my $title = ucfirst($phase).' '.ucfirst($type); my @reports = [qw/Module Want Have/]; for my $mod ( sort keys %{ $req_hash->{$phase}{$type} } ) { next if grep { $_ eq $mod } @exclude; my $want = $req_hash->{$phase}{$type}{$mod}; $want = "undef" unless defined $want; $want = "any" if !$want && $want == 0; if ($mod eq 'perl') { push @reports, ['perl', $want, $]]; next; } my $req_string = $want eq 'any' ? 'any version required' : "version '$want' required"; my $file = $mod; $file =~ s{::}{/}g; $file .= ".pm"; my ($prefix) = grep { -e File::Spec->catfile($_, $file) } @INC; if ($prefix) { my $have = MM->parse_version( File::Spec->catfile($prefix, $file) ); $have = "undef" unless defined $have; push @reports, [$mod, $want, $have]; if ( $DO_VERIFY_PREREQS && $HAS_CPAN_META && $type eq 'requires' ) { if ( $have !~ /\A$lax_version_re\z/ ) { push @dep_errors, "$mod version '$have' cannot be parsed ($req_string)"; } elsif ( ! $full_prereqs->requirements_for( $phase, $type )->accepts_module( $mod => $have ) ) { push @dep_errors, "$mod version '$have' is not in required range '$want'"; } } } else { push @reports, [$mod, $want, "missing"]; if ( $DO_VERIFY_PREREQS && $type eq 'requires' ) { push @dep_errors, "$mod is not installed ($req_string)"; } } } if ( @reports ) { push @full_reports, "=== $title ===\n\n"; my $ml = _max( map { length $_->[0] } @reports ); my $wl = _max( map { length $_->[1] } @reports ); my $hl = _max( map { length $_->[2] } @reports ); if ($type eq 'modules') { splice @reports, 1, 0, ["-" x $ml, "", "-" x $hl]; push @full_reports, map { sprintf(" %*s %*s\n", -$ml, $_->[0], $hl, $_->[2]) } @reports; } else { splice @reports, 1, 0, ["-" x $ml, "-" x $wl, "-" x $hl]; push @full_reports, map { sprintf(" %*s %*s %*s\n", -$ml, $_->[0], $wl, $_->[1], $hl, $_->[2]) } @reports; } push @full_reports, "\n"; } } } if ( @full_reports ) { diag "\nVersions for all modules listed in $source (including optional ones):\n\n", @full_reports; } if ( $cpan_meta_error || @dep_errors ) { diag "\n*** WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING ***\n"; } if ( $cpan_meta_error ) { my ($orig_source) = grep { -f } 'MYMETA.json', 'MYMETA.yml'; diag "\nCPAN::Meta->load_file('$orig_source') failed with: $cpan_meta_error\n"; } if ( @dep_errors ) { diag join("\n", "\nThe following REQUIRED prerequisites were not satisfied:\n", @dep_errors, "\n" ); } pass('Reported prereqs'); # vim: ts=4 sts=4 sw=4 et: 00-report-prereqs.dd100644001750001750 243715131133604 26571 0ustar00sawyersawyer000000000000Perl-Critic-Policy-Variables-ProhibitLoopOnHash-0.009/tdo { my $x = { 'configure' => { 'requires' => { 'ExtUtils::MakeMaker' => '0' } }, 'develop' => { 'requires' => { 'Test::More' => '0.88', 'Test::NoTabs' => '0', 'Test::Pod' => '1.41' } }, 'runtime' => { 'requires' => { 'Carp' => '0', 'List::Util' => '1.33', 'Perl::Critic' => '1.126', 'parent' => '0' } }, 'test' => { 'recommends' => { 'CPAN::Meta' => '2.120900' }, 'requires' => { 'ExtUtils::MakeMaker' => '0', 'File::Spec' => '0', 'Test::More' => '0' } } }; $x; }Variables000755001750001750 015131133604 24613 5ustar00sawyersawyer000000000000Perl-Critic-Policy-Variables-ProhibitLoopOnHash-0.009/tProhibitLoopOnHash.run100644001750001750 444415131133604 31222 0ustar00sawyersawyer000000000000Perl-Critic-Policy-Variables-ProhibitLoopOnHash-0.009/t/Variables## name Basic forward for/foreach ## failures 2 ## cut for (%hash) { foreach (%hash) { ## name forward for/foreach with variable ## failures 2 ## cut for $foo (%hash) { foreach $foo (%hash) { ## name forward for/foreach with lexical variable ## failures 2 ## cut for my $foo (%hash) { foreach my $bar (%hash) { ## name postfix variations ## failures 4 ## cut print for %hash; print for %{$hash}; print for %{ something() }; print foreach %hash; ## name reference variation ## failures 8 ## cut for my $foo (%{$hash}) { foreach my $bar (%$hash) { for $foo (%{$hash}) { foreach $foo (%$hash) { for (%{$hash}) { foreach (%$hash) { print for %{$hash}; print foreach %$hash; ## name Correct patterns ## failures 0 ## cut for my $foo (keys %hash) { for my $foo (values %hash) { foreach my $bar (keys %hash) { for $foo (values %hash) { foreach $foo (keys %hash) { for (values %hash) { foreach (keys %hash) { print for values %hash; print foreach keys %hash; print for ( values %hash ); print foreach (keys %hash); print foreach (keys %{$hash}); print foreach (keys %{ some_big_thing() }); for my $k (%foo ? sort keys %foo : sort keys %bar) { for my $k (%{$foo} ? sort keys %{$foo} : sort keys %{$bar}) { for my $k (%{ func() } ? sort keys %{ func() } : sort keys %{ func() }) { ## name Using "for"/"foreach" as hash key ## failures 0 ## cut $hash{for} $hash->{for} $hash->{ for } $hash{foreach} $hash->{foreach} $hash->{ foreach } # These came from Ahmed Abdrabo and Andy Lester ## name thing() for @array statement ## failures 0 ## cut print for @INC say for @INC END { $dbh->do("DROP TABLE $_") foreach @table } { print foreach @y } { print foreach 1..2 } { print foreach @y; } { print } print foreach @y; ## name thing() for %hash statement ## failures 7 ## cut print for %INC say for %INC END { $dbh->do("DROP TABLE $_") foreach %tables } { print foreach %y } { print foreach %{@y} } { print foreach %y; } print foreach %y; ## name Variable attributes ## failures 1 ## cut # this is fine for my Dog $foo (@bar) { } for my Foo::Bar $foo (@bar) { } # fails for my Foo::Bar $foo (%bar) { } ## name Reference Aliasing ## failures 0 ## cut foreach \my %hash (@array_of_hash_references) {} ## name Empty postfix loop ## failures 0 ## cut for Class->method($foo); ## name syntax error ## failures 0 ## cut for my $foo @list { Variables000755001750001750 015131133604 30474 5ustar00sawyersawyer000000000000Perl-Critic-Policy-Variables-ProhibitLoopOnHash-0.009/lib/Perl/Critic/PolicyProhibitLoopOnHash.pm100644001750001750 1652615131133604 34737 0ustar00sawyersawyer000000000000Perl-Critic-Policy-Variables-ProhibitLoopOnHash-0.009/lib/Perl/Critic/Policy/Variablespackage Perl::Critic::Policy::Variables::ProhibitLoopOnHash; our $AUTHORITY = 'cpan:XSAWYERX'; # ABSTRACT: Don't write loops on hashes, only on keys and values of hashes $Perl::Critic::Policy::Variables::ProhibitLoopOnHash::VERSION = '0.009'; use strict; use warnings; use parent 'Perl::Critic::Policy'; use Carp qw< croak >; use Perl::Critic::Utils qw< :severities :classification :ppi >; use List::Util 'first'; use constant 'DESC' => 'Looping over hash instead of hash keys or values'; use constant 'EXPL' => 'You are accidentally looping over the hash itself ' . '(both keys and values) ' . 'instead of only keys or only values'; # \bfor(each)?(\s+my)?\s*\$\w+\s*\(\s*% sub supported_parameters { () } sub default_severity { $SEVERITY_HIGH } sub default_themes { 'bugs' } sub applies_to { 'PPI::Token::Word' } sub violates { my ($self, $elem) = @_; first { $elem eq $_ } qw< for foreach > or return (); # This is how we do it: # * First, we clear out scoping (like "my" for "foreach my ...") # * Second, we clear out topical variables ("foreach $foo (...)") # * Then we check if it's a postfix without parenthesis # * Lastly, we handle the remaining cases # Skip if we do not have the right type of PPI::Statement # For example, "$var->{for}" has a PPI::Statement::Expression # when leading for() is a PPI::Statement::Compound and # a postfix for() is a PPI::Statement # This was originally written as: $elem->snext_sibling or return $elem->parent && $elem->parent->isa('PPI::Statement::Expression') and return (); # for \my %foo if ( !$elem->snext_sibling ) { my $next = $elem->next_token; # exhaust spaces $next = $next->next_token while $next->isa('PPI::Token::Whitespace'); # skip the \ if ( $next eq '\\' ) { $elem = $next->next_token; } } # for Class->method($foo) # PPI::Document # PPI::Statement::Compound # PPI::Token::Word 'for' # PPI::Token::Whitespace ' ' # PPI::Statement # PPI::Token::Word 'Class' # PPI::Token::Operator '->' # PPI::Token::Word 'method' # PPI::Structure::List ( ... ) # PPI::Statement::Expression # PPI::Token::Symbol '$foo' # PPI::Token::Structure ';' if ( !$elem->snext_sibling && $elem->next_token) { # exhaust spaces $elem = $elem->next_token while $elem->next_token->isa('PPI::Token::Whitespace'); # just move to next token and continue from there $elem->next_token and $elem = $elem->next_token; } # for my $foo (%hash) # we simply skip the "my" if ( ( my $scope = $elem->snext_sibling )->isa('PPI::Token::Word') ) { if ( first { $scope eq $_ } qw< my our local state > ) { # for my Foo::Bar $baz (%hash) # PPI doesn't handle this well # as you can see from the following dump: # PPI::Statement::Compound # PPI::Token::Word 'for' # PPI::Token::Whitespace ' ' # PPI::Token::Word 'my' # PPI::Token::Whitespace ' ' # PPI::Statement # PPI::Token::Word 'Foo::BAR' # PPI::Token::Whitespace ' ' # PPI::Token::Symbol '$payment' # PPI::Token::Whitespace ' ' # PPI::Structure::List ( ... ) # PPI::Statement::Expression # PPI::Token::Symbol '@bar' # PPI::Token::Whitespace ' ' # PPI::Structure::Block { ... } # PPI::Token::Whitespace ' ' # First, we need to exhaust spaces my $next = $scope; $next = $next->next_token while $next->next_token->isa('PPI::Token::Whitespace'); # Then we can use 'next_token' to jump to the next one, # even if it's not a sibling $elem = $next->next_token; # And if it's a variable attribute, we skip it $elem->isa('PPI::Token::Word') and $elem = $elem->snext_sibling; } else { # for keys %hash # for Class->method($foo) } } my $topical = $elem->snext_sibling or return (); # for $foo (%hash) # we simply skip the "$foo" if ( $topical->isa('PPI::Token::Symbol') ) { if ( $topical->snext_sibling && $topical->snext_sibling->isa('PPI::Structure::List') ) { $elem = $topical; $topical = $elem->snext_sibling or return (); } else { # for $foo (%hash); } } # for %hash # (postfix without parens) _check_symbol_or_cast( $topical ) and return $self->violation( DESC(), EXPL(), $elem ); # for (%hash) if ( $topical->isa('PPI::Structure::List') ) { my @children = $topical->schildren; @children > 1 and croak "List has multiple significant children ($topical)"; if ( ( my $statement = $children[0] )->isa('PPI::Statement') ) { my @statement_args = $statement->schildren; _check_symbol_or_cast( $statement_args[0] ) and return $self->violation( DESC(), EXPL(), $statement ); } } return (); } sub _check_symbol_or_cast { my $arg = shift; # This is either a variable # or casting from a variable (or from a statement) $arg->isa('PPI::Token::Symbol') && $arg =~ /^%/xms or $arg->isa('PPI::Token::Cast') && $arg eq '%' or return; my $next_op = $arg->snext_sibling; # If this is a cast, we want to exhaust the block # the block could include anything, really... if ( $arg->isa('PPI::Token::Cast') && $next_op->isa('PPI::Structure::Block') ) { $next_op = $next_op->snext_sibling; } # Safe guard against operators # for ( %hash ? ... : ... ); $next_op && $next_op->isa('PPI::Token::Operator') and return; return 1; } 1; __END__ =pod =encoding UTF-8 =head1 NAME Perl::Critic::Policy::Variables::ProhibitLoopOnHash - Don't write loops on hashes, only on keys and values of hashes =head1 VERSION version 0.009 =head1 DESCRIPTION When "looping over hashes," we mean looping over hash keys or hash values. If you forgot to call C or C you will accidentally loop over both. foreach my $foo (%hash) {...} # not ok action() for %hash; # not ok foreach my $foo ( keys %hash ) {...} # ok action() for values %hash; # ok An effort is made to detect expressions: action() for %hash ? keys %hash : (); # ok action() for %{ $hash{'stuff'} } ? keys %{ $hash{'stuff'} } : (); # ok (Granted, the second example there doesn't make much sense, but I have found a variation of it in real code.) =head1 CONFIGURATION This policy is not configurable except for the standard options. =head1 AUTHOR Sawyer X, C =head1 THANKS Thank you to Ruud H.G. Van Tol. =head1 SEE ALSO L =head1 AUTHOR Sawyer X =head1 COPYRIGHT AND LICENSE This software is Copyright (c) 2026 by Sawyer X. This is free software, licensed under: The MIT (X11) License =cut