Repository: cucumber/gherkin
Pull request #392: follow some Perl Best Pratices
Author: login: fperrad
Created: 2025-05-13T12:27:02Z | Merged: 2025-05-21T14:58:41Z

<!---
Thanks for helping to make Cucumber better! 💖

You can feel free to open a "Draft" status pull request if you're not ready for feedback yet.

Don't worry about getting everything perfect! We're here to help and will coach you through
to getting your pull request ready to merge.

The prompts below are for guidance to help you describe your change in a way that is most 
likely to make sense to other people when they are reviewing it. Still, it's just a guide, 
so feel free to delete anything that doesn't feel appropriate, and add anything additional 
that seems like it would provide useful context. 👏🏻
-->

### 🤔 What's changed?

<!-- Describe your changes in detail -->

refactor Perl modules

### ⚡️ What's your motivation? 

<!-- 
What motivated you to propose this change? Does it fix a bug? Add a new feature?
If it fixes an open issue, you can link to the issue here, e.g. "Fixes #99"
-->

sanify Perl code by following some Perl Best Practices.

the tool [perlcritic](https://metacpan.org/pod/perlcritic) is used for this.

### 🏷️ What kind of change is this?

- :bank: Refactoring/debt/DX (improvement to code design, tooling, etc. without changing behaviour)

### ♻️ Anything particular you want feedback on?

<!-- 
Is there anything in this change you're unsure about, or would 
particularly like reviewers to give you feedback on?
-->

### 📋 Checklist:

<!--- 
This is to help you remember all the little things we often forget to do!

Feel free to delete any tasks that are not relevant, or add new ones.
-->

- [x] I agree to respect and uphold the [Cucumber Community Code of Conduct](https://github.com/cucumber/.github/tree/main?tab=coc-ov-file)
- [ ] I've changed the behaviour of the code
  - [ ] I have added/updated tests to cover my changes.
- [ ] My change requires a change to the documentation.
  - [ ] I have updated the documentation accordingly.
- [x] Users should know about my change
  - [x] I have added an entry to the "Unreleased" section of the [**CHANGELOG**](../blob/main/CHANGELOG.md), linking to this pull request.

----

*This text was originally generated from a [template](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/about-issue-and-pull-request-templates), then edited by hand. [You can modify the template here.](https://github.com/cucumber/.github/edit/main/.github/PULL_REQUEST_TEMPLATE.md)*



Diff:
diff --git a/perl/gherkin-perl.razor b/perl/gherkin-perl.razor
index a5554b626..ef7bf8451 100644
--- a/perl/gherkin-perl.razor
+++ b/perl/gherkin-perl.razor
@@ -35,12 +35,14 @@ use warnings;
 
 use base 'Gherkin::ParserBase';
 
-our %states_to_match_names = (
+## no critic (ProhibitMagicNumbers)
+my %states_to_match_names = (
     @foreach(var state in Model.States.Values.Where(s => !s.IsEndState)) //..
     {
 @:    @state.Id => "match_token_at_@(state.Id)",
     }
 );
+## use critic
 
 sub parse {
     my ( $self, $token_scanner, $uri ) = @@_;
@@ -94,6 +96,7 @@ sub _construct_parser_error {
     return $error_class->new( $token, @@args );
 }
 
+## no critic (ProhibitExcessComplexity, ProhibitMagicNumbers, ProhibitPostfixControls)
 @foreach(var state in Model.States.Values.Where(s => !s.IsEndState)) //..
 {<text>
 # @Raw(state.Comment)
@@ -126,6 +129,7 @@ sub match_token_at_@(state.Id) {
 </text>
 }
 
+## use critic
 @foreach(var lookAheadHint in Model.RuleSet.LookAheadHints)
 {
 <text>
diff --git a/perl/lib/Gherkin.pm b/perl/lib/Gherkin.pm
index b3083ef5a..08d1c6866 100644
--- a/perl/lib/Gherkin.pm
+++ b/perl/lib/Gherkin.pm
@@ -32,15 +32,17 @@ sub from_paths {
     my ($class, $paths, $id_generator, $sink, %options) = @_;
 
     my $gherkin = $class->new(%options);
-    for my $path (@$paths) {
+    for my $path (@{$paths}) {
         # Note: There's a huge difference between ':utf8' and
         # ':encoding(UTF-8)' in Perl: the latter causes strict UTF-8 conversion
         # and fails hard if there are encoding problems. The former
         # accommodates the errors and simply continues, allowing us to
         # recode back to octets and then to the encoding indicated in the
         # header using the "# encoding: ..." header.
+        ## no critic (RequireEncodingWithUTF8Layer)
         open my $fh, '<:utf8', $path
             or die "Unable to open gherkin document $path: $!";
+        ## use critic
 
         # local $/ = undef; --> unset 'end-of-line' marker: slurp entire file
         # use the 'do' block to scope this binding to smallest possible scope
diff --git a/perl/lib/Gherkin/AstBuilder.pm b/perl/lib/Gherkin/AstBuilder.pm
index 515740b5e..b044724ca 100644
--- a/perl/lib/Gherkin/AstBuilder.pm
+++ b/perl/lib/Gherkin/AstBuilder.pm
@@ -121,11 +121,11 @@ sub get_table_rows {
 
 sub ensure_cell_count {
     my ( $self, $rows ) = @_;
-    return unless @$rows;
+    return unless @{$rows};
 
     my $cell_count;
 
-    for my $row (@$rows) {
+    for my $row (@{$rows}) {
         my $this_row_count = @{ $row->cells };
         $cell_count = $this_row_count unless defined $cell_count;
         unless ( $cell_count == $this_row_count ) {
@@ -160,6 +160,7 @@ sub next_id {
     return $self->{'id_generator'}->();
 }
 
+## no critic (ProhibitExcessComplexity, ProhibitCascadingIfElse)
 sub transform_node {
     my ( $self, $node ) = @_;
 
@@ -182,7 +183,7 @@ sub transform_node {
         my $media_type      = $separator_token->matched_text;
         my $delimiter       = $separator_token->matched_keyword;
         my $line_tokens     = $node->get_tokens('Other');
-        my $content = join( "\n", map { $_->matched_text } @$line_tokens );
+        my $content = join( "\n", map { $_->matched_text } @{$line_tokens} );
 
         return Cucumber::Messages::DocString->new(
             location    => $self->get_location($separator_token),
@@ -353,5 +354,6 @@ sub transform_node {
         return $node;
     }
 }
+## use critic
 
 1;
diff --git a/perl/lib/Gherkin/Dialect.pm b/perl/lib/Gherkin/Dialect.pm
index d0ee0c4d9..9ecc62f0f 100644
--- a/perl/lib/Gherkin/Dialect.pm
+++ b/perl/lib/Gherkin/Dialect.pm
@@ -23,8 +23,10 @@ sub new {
             close $fh;
             $options->{'dictionary'} = Cpanel::JSON::XS::decode_json($input);
         } else {
+            ## no critic (ProhibitPackageVars)
             require Gherkin::Generated::Languages;
             $options->{'dictionary'} = $Gherkin::Generated::Languages::data;
+            ## use critic
         }
     }
 
diff --git a/perl/lib/Gherkin/Exceptions.pm b/perl/lib/Gherkin/Exceptions.pm
index 13167d8ee..7c39eea4f 100644
--- a/perl/lib/Gherkin/Exceptions.pm
+++ b/perl/lib/Gherkin/Exceptions.pm
@@ -1,3 +1,5 @@
+## no critic (ProhibitMultiplePackages, RequireExplicitPackage)
+
 use strict;
 use warnings;
 
diff --git a/perl/lib/Gherkin/Generated/Parser.pm b/perl/lib/Gherkin/Generated/Parser.pm
index 967a82ade..603c13f36 100644
--- a/perl/lib/Gherkin/Generated/Parser.pm
+++ b/perl/lib/Gherkin/Generated/Parser.pm
@@ -6,7 +6,8 @@ use warnings;
 
 use base 'Gherkin::ParserBase';
 
-our %states_to_match_names = (
+## no critic (ProhibitMagicNumbers)
+my %states_to_match_names = (
     0 => "match_token_at_0",
     1 => "match_token_at_1",
     2 => "match_token_at_2",
@@ -50,6 +51,7 @@ our %states_to_match_names = (
     41 => "match_token_at_41",
     42 => "match_token_at_42",
 );
+## use critic
 
 sub parse {
     my ( $self, $token_scanner, $uri ) = @_;
@@ -103,6 +105,7 @@ sub _construct_parser_error {
     return $error_class->new( $token, @args );
 }
 
+## no critic (ProhibitExcessComplexity, ProhibitMagicNumbers, ProhibitPostfixControls)
 
 # Start
 sub match_token_at_0 {
@@ -3879,6 +3882,7 @@ sub match_token_at_42 {
     return 42;
 }
 
+## use critic
 
 sub lookahead_0 {
     my ($self, $context, $current_token) = @_;
diff --git a/perl/lib/Gherkin/Line.pm b/perl/lib/Gherkin/Line.pm
index 2c311513c..4fac33ce1 100644
--- a/perl/lib/Gherkin/Line.pm
+++ b/perl/lib/Gherkin/Line.pm
@@ -121,7 +121,7 @@ sub table_cells {
         $stripped_cell =~ s/\s+$//;
         $stripped_cell =~ s/(\\\\|\\\||\\n)/$unescape_map{$1}/g;
         push(
-            @$cells,
+            @{$cells},
             {
                 column => $col + $self->indent + $cell_indent,
                 text   => $stripped_cell
diff --git a/perl/lib/Gherkin/ParserBase.pm b/perl/lib/Gherkin/ParserBase.pm
index 6e15ce95d..3644a04e0 100644
--- a/perl/lib/Gherkin/ParserBase.pm
+++ b/perl/lib/Gherkin/ParserBase.pm
@@ -42,6 +42,10 @@ sub add_error {
       if @errors > $self->max_errors;
 }
 
+## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
+# Private subs (subs prefixed with an underscore) are used as "friend methods" here,
+# to be used by the actual parser class which is based on this one.
+
 sub _start_rule {
     my ( $self, $context, $ruleType ) = @_;
 
diff --git a/perl/lib/Gherkin/Pickles/Compiler.pm b/perl/lib/Gherkin/Pickles/Compiler.pm
index 760fd6b9a..6995061aa 100644
--- a/perl/lib/Gherkin/Pickles/Compiler.pm
+++ b/perl/lib/Gherkin/Pickles/Compiler.pm
@@ -78,7 +78,7 @@ sub _compile_scenario {
         }
     }
 
-    my @all_tags = (@$tags, @{ $scenario->tags || [] });
+    my @all_tags = (@{$tags}, @{ $scenario->tags || [] });
     $pickle_sink->(
         Cucumber::Messages::Envelope->new(
             pickle => Cucumber::Messages::Pickle->new(
@@ -101,7 +101,7 @@ sub _compile_scenario_outline {
         my @tags        = (
             @{ $feature_tags },
             @{ $scenario->tags || [] },
-            @{ $examples->tags || [] }
+            @{ $examples->tags || [] },
             );
         my $variables = $examples->table_header->cells;
 
@@ -152,7 +152,7 @@ sub _compile_rule {
     my ( $class, $uri, $feature_tags, $feature_background_steps,
          $rule_definition, $language, $id_generator, $pickle_sink )
         = @_;
-    my @background_steps = ( @$feature_background_steps );
+    my @background_steps = ( @{$feature_background_steps} );
     my @tags = (
         @{ $feature_tags || [] },
         @{ $rule_definition->tags || [] }
@@ -197,7 +197,7 @@ sub _pickle_step_props {
         ast_node_ids => [ $step->id ],
         type         => $keyword_type,
         text         => $class->_interpolate($step->text,
-                                             $variables, $value_cells)
+                                             $variables, $value_cells),
         );
 
     if ($values) {
@@ -241,7 +241,7 @@ sub _pickle_step_props {
 
 sub _pickle_tags {
     my ( $class, $tags ) = @_;
-    return [ map { $class->_pickle_tag( $_ ) } @$tags ];
+    return [ map { $class->_pickle_tag( $_ ) } @{$tags} ];
 }
 
 sub _pickle_tag {
diff --git a/perl/lib/Gherkin/TokenMatcher.pm b/perl/lib/Gherkin/TokenMatcher.pm
index d2eac7332..9d1be95ed 100644
--- a/perl/lib/Gherkin/TokenMatcher.pm
+++ b/perl/lib/Gherkin/TokenMatcher.pm
@@ -27,7 +27,7 @@ sub new {
 sub _add_keyword_type_mappings {
     my ($keyword_types, $keywords, $type) = @_;
 
-    for my $keyword (@$keywords) {
+    for my $keyword (@{$keywords}) {
         if (not exists $keyword_types->{$keyword}) {
             $keyword_types->{$keyword} = [];
         }
@@ -122,7 +122,7 @@ sub _match_title_line {
     my ( $self, $token, $token_type, $keywords ) = @_;
     return unless $token->line;
 
-    for my $keyword (@$keywords) {
+    for my $keyword (@{$keywords}) {
         if ( $token->line->startswith_title_keyword($keyword) ) {
             my $title =
               $token->line->get_rest_trimmed( length( $keyword . ': ' ) );
diff --git a/perl/lib/Gherkin/TokenScanner.pm b/perl/lib/Gherkin/TokenScanner.pm
index 2407951d3..8a3a67737 100644
--- a/perl/lib/Gherkin/TokenScanner.pm
+++ b/perl/lib/Gherkin/TokenScanner.pm
@@ -16,6 +16,9 @@ sub new {
 
     # Perl convention is that a string reference is the string itself, but that
     # a straight string is a path
+    ## no critic (RequireBriefOpen)
+    # the file is opened in this object constructor,
+    # and will be closed in the method next_line at the end
     my $fh;
     if ( ref $path_or_str eq 'SCALAR' ) {
         my $bytes = Encode::encode('UTF-8', ${ $path_or_str });
@@ -25,6 +28,7 @@ sub new {
           || croak "Can't open [$path_or_str] for reading";
         $fh->binmode(':utf8');
     }
+    ## use critic
 
     return bless { fh => $fh, line_number => 0 }, $class;
 }
diff --git a/perl/t/020_parser_instantiation.t b/perl/t/020_parser_instantiation.t
index 984f367df..6a1b82c2c 100644
--- a/perl/t/020_parser_instantiation.t
+++ b/perl/t/020_parser_instantiation.t
@@ -22,7 +22,7 @@ for (
     [ stringref => \$content ], # By content
     [ scanner => Gherkin::TokenScanner->new( '' . $file ) ], # Object
 ) {
-    my ( $type, $input ) = @$_;
+    my ( $type, $input ) = @{$_};
     $results{ $type } = Gherkin::Parser->new->parse( $input );
     ok( 'true', "Parsing via $type lived" );
 }
diff --git a/perl/t/030_tokenscanner.t b/perl/t/030_tokenscanner.t
index 6a8e298a4..0182b8f18 100644
--- a/perl/t/030_tokenscanner.t
+++ b/perl/t/030_tokenscanner.t
@@ -10,7 +10,7 @@ use Gherkin::TokenScanner;
 
 ## Test the token scanner to report empty lines correctly
 
-my $feature = <<FEATURE;
+my $feature = <<'FEATURE';
 # comment line
 Feature: Testing an empty line
 


Reviews:
user: login: mpkorstanje
body: Looks okay, but I' not fluent in Perl. I'll wait for a review from @ehuelsmann.

Small remark about the changelog.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T12:54:38Z
user: login: ehuelsmann
body: Thanks for the contribution! There's a lot of idiomatic Perl cleanup here.

Please do note however, that the orginal goal of the module was not to write the most idiomatic Perl, but to write Perl in a way that non-Perl coders would understand the code. In that context, I'm not 100% sure that the removal of the parens that I'm seeing in several places, is an improvement.

I've requested a few changes be made to simplify readability of the code here and there. Please change, or explain what your concerns with my proposal are. Note that none of my suggestions have been run (I've entered them straight into the GitHub UI).
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T14:23:41Z
user: login: ehuelsmann
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T14:24:42Z
user: login: fperrad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T15:41:57Z
user: login: fperrad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T19:21:32Z
user: login: fperrad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T19:22:32Z
user: login: fperrad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T19:22:59Z
user: login: fperrad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T19:23:32Z
user: login: fperrad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T19:24:09Z
user: login: fperrad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T19:24:42Z
user: login: fperrad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T19:25:39Z
user: login: ehuelsmann
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T20:37:52Z
user: login: fperrad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T08:00:53Z
user: login: fperrad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T08:01:08Z
user: login: fperrad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T08:16:24Z
user: login: ehuelsmann
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T16:04:48Z
user: login: ehuelsmann
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T16:10:29Z
user: login: ehuelsmann
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T16:13:02Z
user: login: simcop2387
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-15T17:26:03Z
user: login: ehuelsmann
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T18:10:33Z
user: login: fperrad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T20:08:17Z
user: login: fperrad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T20:29:38Z
user: login: fperrad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T20:32:40Z
user: login: ehuelsmann
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T20:39:48Z
user: login: ehuelsmann
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T20:42:04Z
user: login: fperrad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T19:17:02Z
user: login: ehuelsmann
body: Yep. Definitely readability improvements and comprehensible code structure.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T21:59:17Z

Review comments:
diff_hunk: @@ -7,6 +7,8 @@ This project adheres to [Semantic Versioning](http://semver.org).
 This document is formatted according to the principles of [Keep A CHANGELOG](http://keepachangelog.com).
 
 ## [Unreleased]
+### Changed
+- [Perl] Follow some Perl Best Practices (found by perlcritic)
path: CHANGELOG.md
user: login: mpkorstanje
body: I assume there is no meaningful change for the end user. If so, this can be omitted.
created_at: 2025-05-13T12:53:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,6 +18,7 @@ sub run {
     print $fh join "\n", @{ $parser->parse($_) } for @file_list;
     print $fh "\n";
 
+    return;
path: perl/bin/gherkin-generate-tokens
user: login: ehuelsmann
body: Ok. Fair enough. This is a matter of style; other perl guides indicate that "return undef;" should be used, because the "undef" is implicit now. But having a return statement at the end of a "sub" is indeed better than making even that implicit.
created_at: 2025-05-13T13:38:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,8 +18,8 @@
 {<text>    $token->detach;
     $err = $self->_construct_parser_error(
         $token,
-        ["@Raw(string.Join("\", \"", expectedTokens))"],
-        "State: @state.Id - @Raw(state.Comment)",
+        ['@Raw(string.Join("', '", expectedTokens))'],
+        'State: @state.Id - @Raw(state.Comment)',
     );
path: perl/gherkin-perl.razor
user: login: ehuelsmann
body: Single quotes don't interpolate, agreed; generally advised for performance reasons, but on the size of our code base not very relevant. Good for style adherence.
created_at: 2025-05-13T13:39:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,12 +35,14 @@ use warnings;
 
 use base 'Gherkin::ParserBase';
 
-our %states_to_match_names = (
+## no critic (ProhibitMagicNumbers)
path: perl/gherkin-perl.razor
user: login: ehuelsmann
body: I think it makes sense to disable the magic numbers policy in general, not just locally: generated code bases have a tendency to use magic numbers (all over the place).
created_at: 2025-05-13T13:45:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,12 +35,14 @@ use warnings;
 
 use base 'Gherkin::ParserBase';
 
-our %states_to_match_names = (
+## no critic (ProhibitMagicNumbers)
+my %states_to_match_names = (
path: perl/gherkin-perl.razor
user: login: ehuelsmann
body: Fair enough. This was a `our` (more-or-less public) variable when I inherited the code base. I like making this a private variable.
created_at: 2025-05-13T13:46:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -83,17 +85,18 @@ sub match_token {
     my ( $self, $state, $token, $context ) = @@_;
     my $method_name = $states_to_match_names{ $state } ||
         die "Unknown state: $state";
-    $self->$method_name( $token, $context );
+    return $self->$method_name( $token, $context );
 }
 
 sub _construct_parser_error {
     my ($self, $token, @@args) = @@_;
-    my $error_class = "Gherkin::Exceptions::" . (
+    my $error_class = 'Gherkin::Exceptions::' . (
         $token->is_eof ? 'UnexpectedEOF' : 'UnexpectedToken' );
path: perl/gherkin-perl.razor
user: login: ehuelsmann
body: To be honest, if the point is to have simple code, I think the following should replace the entire function:

```perl
sub _construct_parser_error {
    my ($self, $token, @@args) = @@_;
    if ($token->is_eof) {
       return Gherkin::Exceptions::UnexpectedEOF->new( $token, @@args );
    }
    else {
       return Gherkin::Exceptions::UnexpectedToken->new( $token, @@args );
    }
}
created_at: 2025-05-13T13:52:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,7 +72,7 @@ sub formatter {
     my $fh   = $self->out_handle;
     return sub {
         my $msg = shift;
-        print $fh $msg->to_json . "\n";
+        print {$fh} $msg->to_json . "\n";
path: perl/lib/App/gherkin.pm
user: login: ehuelsmann
body: I don't understand this change. The original code is actually how you write a file for which the file handle is stored in the `$fh` variable. I'm against this change.

Please change back to the original:

```suggestion
        print $fh $msg->to_json . "\n";
```
created_at: 2025-05-13T13:57:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -130,11 +132,10 @@ sub new {
 
 sub detailed_message {
     my $self = shift;
-    return sprintf(
-        "expected: %s, got '%s'",
+    return sprintf
+        'expected: %s, got \'%s\'',
path: perl/lib/Gherkin/Exceptions.pm
user: login: ehuelsmann
body: Please change to:

```suggestion
        q{expected: %s, got '%s'},
```

I don't think having the escaped single quotes makes the code better than having double quotes; the suggestion above removes the need to escape the single quotes at the expense of making the code more idiomatic Perl, but less "simplified Perl".
created_at: 2025-05-13T14:03:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -140,8 +139,8 @@ sub tags {
 
     my @tags;
     my @errors;
-    my @items = split( /@/, $items_line );
-    shift(@items);    # Blank first item
+    my @items = split /@/, $items_line;
+    shift @items;    # Blank first item
path: perl/lib/Gherkin/Line.pm
user: login: ehuelsmann
body: ```suggestion
    my (undef, @items) = split /@/, $items_line; # Drop blank first item
```

If we're going with idiomatic Perl, the way to throw away the leading element of an array is by putting "undef" at the beginning of the assignment list. 
created_at: 2025-05-13T14:06:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,28 +40,34 @@ sub add_error {
     my @errors = $context->errors;
     Gherkin::Exceptions::CompositeParser->throw(@errors)
       if @errors > $self->max_errors;
+    return;
 }
 
+## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
+
path: perl/lib/Gherkin/ParserBase.pm
user: login: ehuelsmann
body: ```suggestion
# Private subs (subs prefixed with an underscore) are used as "friend methods" here,
# to be used by the actual parser class which is based on this one.
```
created_at: 2025-05-13T14:12:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -51,46 +52,47 @@ sub change_dialect {
                                [ @{ $self->dialect->And }, @{ $self->dialect->But } ],
                                Cucumber::Messages::Step::KEYWORDTYPE_CONJUNCTION);
     $self->_keyword_types( $keyword_types );
+    return;
 }
 
 sub reset {
     my $self = shift;
     $self->change_dialect( $self->_default_dialect_name );
     $self->_indent_to_remove(0);
     $self->_active_doc_string_separator(undef);
-
+    return;
 }
 
 sub match_FeatureLine {
     my ( $self, $token ) = @_;
-    $self->_match_title_line( $token, FeatureLine => $self->dialect->Feature );
+    return $self->_match_title_line( $token, FeatureLine => $self->dialect->Feature );
 }
 
 sub match_RuleLine {
     my ( $self, $token ) = @_;
-    $self->_match_title_line( $token,
+    return $self->_match_title_line( $token,
         RuleLine => $self->dialect->Rule );
 }
 
 sub match_ScenarioLine {
     my ( $self, $token ) = @_;
-    $self->_match_title_line(
+    return $self->_match_title_line(
         $token,
         ScenarioLine => $self->dialect->Scenario )
-        or $self->_match_title_line(
+        || $self->_match_title_line(
path: perl/lib/Gherkin/TokenMatcher.pm
user: login: ehuelsmann
body: We really want more simplistic Perl code, for any non-Perl coder to understand. I think we should instead write:

```perl
sub match_ScenarioLine {
    my ( $self, $token ) = @_;

    if (my $match = $self->_match_title_line(
        $token,
        ScenarioLine => $self->dialect->Scenario )) {
       return $match;
    }
    return $self->_match_title_line(
            $token,
            ScenarioLine => $self->dialect->ScenarioOutline );
}
```
created_at: 2025-05-13T14:17:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -266,6 +269,7 @@ sub _match_DocStringSeparator {
 
     $self->_set_token_matched( $token,
         DocStringSeparator => { text => $content_type, keyword => $separator } );
+    return 1;
path: perl/lib/Gherkin/TokenMatcher.pm
user: login: ehuelsmann
body: why '1'? Why not return the return value of `$self->_set_token_matched()` as was returned before?
created_at: 2025-05-13T14:18:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -274,6 +278,7 @@ sub match_TableRow {
 
     $self->_set_token_matched( $token,
         TableRow => { items => $token->line->table_cells } );
+    return 1;
path: perl/lib/Gherkin/TokenMatcher.pm
user: login: ehuelsmann
body: Same question here.
created_at: 2025-05-13T14:18:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,6 +16,7 @@ sub new {
 
     # Perl convention is that a string reference is the string itself, but that
     # a straight string is a path
+    ## no critic (RequireBriefOpen)
path: perl/lib/Gherkin/TokenScanner.pm
user: login: ehuelsmann
body: Please add a small explanation why this policy should not be applied here.
created_at: 2025-05-13T14:19:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -7,6 +7,8 @@ This project adheres to [Semantic Versioning](http://semver.org).
 This document is formatted according to the principles of [Keep A CHANGELOG](http://keepachangelog.com).
 
 ## [Unreleased]
+### Changed
+- [Perl] Follow some Perl Best Practices (found by perlcritic)
path: CHANGELOG.md
user: login: ehuelsmann
body: At this point, there's nothing for the end user, indeed.
created_at: 2025-05-13T14:24:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,12 +35,14 @@ use warnings;
 
 use base 'Gherkin::ParserBase';
 
-our %states_to_match_names = (
+## no critic (ProhibitMagicNumbers)
path: perl/gherkin-perl.razor
user: login: fperrad
body: this local statement `ProhibitMagicNumbers` is only needed 2 times in `gherkin-perl.razor` and never in hand-written modules.
So, I prefer local to global.
created_at: 2025-05-13T15:41:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,7 +72,7 @@ sub formatter {
     my $fh   = $self->out_handle;
     return sub {
         my $msg = shift;
-        print $fh $msg->to_json . "\n";
+        print {$fh} $msg->to_json . "\n";
path: perl/lib/App/gherkin.pm
user: login: fperrad
body: see https://metacpan.org/pod/Perl::Critic::Policy::InputOutput::RequireBracedFileHandleWithPrint
created_at: 2025-05-13T19:21:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,8 +18,8 @@
 {<text>    $token->detach;
     $err = $self->_construct_parser_error(
         $token,
-        ["@Raw(string.Join("\", \"", expectedTokens))"],
-        "State: @state.Id - @Raw(state.Comment)",
+        ['@Raw(string.Join("', '", expectedTokens))'],
+        'State: @state.Id - @Raw(state.Comment)',
     );
path: perl/gherkin-perl.razor
user: login: fperrad
body: commit removed
created_at: 2025-05-13T19:22:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -83,17 +85,18 @@ sub match_token {
     my ( $self, $state, $token, $context ) = @@_;
     my $method_name = $states_to_match_names{ $state } ||
         die "Unknown state: $state";
-    $self->$method_name( $token, $context );
+    return $self->$method_name( $token, $context );
 }
 
 sub _construct_parser_error {
     my ($self, $token, @@args) = @@_;
-    my $error_class = "Gherkin::Exceptions::" . (
+    my $error_class = 'Gherkin::Exceptions::' . (
         $token->is_eof ? 'UnexpectedEOF' : 'UnexpectedToken' );
path: perl/gherkin-perl.razor
user: login: fperrad
body: commit removed
created_at: 2025-05-13T19:22:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -130,11 +132,10 @@ sub new {
 
 sub detailed_message {
     my $self = shift;
-    return sprintf(
-        "expected: %s, got '%s'",
+    return sprintf
+        'expected: %s, got \'%s\'',
path: perl/lib/Gherkin/Exceptions.pm
user: login: fperrad
body: commits removed
created_at: 2025-05-13T19:23:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -140,8 +139,8 @@ sub tags {
 
     my @tags;
     my @errors;
-    my @items = split( /@/, $items_line );
-    shift(@items);    # Blank first item
+    my @items = split /@/, $items_line;
+    shift @items;    # Blank first item
path: perl/lib/Gherkin/Line.pm
user: login: fperrad
body: commit removed
created_at: 2025-05-13T19:24:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,28 +40,34 @@ sub add_error {
     my @errors = $context->errors;
     Gherkin::Exceptions::CompositeParser->throw(@errors)
       if @errors > $self->max_errors;
+    return;
 }
 
+## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
+
path: perl/lib/Gherkin/ParserBase.pm
user: login: fperrad
body: comment added
created_at: 2025-05-13T19:24:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -16,6 +16,7 @@ sub new {
 
     # Perl convention is that a string reference is the string itself, but that
     # a straight string is a path
+    ## no critic (RequireBriefOpen)
path: perl/lib/Gherkin/TokenScanner.pm
user: login: fperrad
body: comment added
created_at: 2025-05-13T19:25:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,12 +35,14 @@ use warnings;
 
 use base 'Gherkin::ParserBase';
 
-our %states_to_match_names = (
+## no critic (ProhibitMagicNumbers)
path: perl/gherkin-perl.razor
user: login: ehuelsmann
body: Ok. Lets go with local for now.
created_at: 2025-05-13T20:37:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -266,6 +269,7 @@ sub _match_DocStringSeparator {
 
     $self->_set_token_matched( $token,
         DocStringSeparator => { text => $content_type, keyword => $separator } );
+    return 1;
path: perl/lib/Gherkin/TokenMatcher.pm
user: login: fperrad
body: each `sub match_SomeThing` must return a value which is interpreted as an boolean.
the simplest is to use:
 - `return;` interpreted as FALSE (ie. not match)
 - `return 1;` interpreted as TRUE (ie. match)

when `$self->_set_token_matched()` is the last statement, the returned value is the returned value of `$self->_set_token_matched()` which is the returned value of `$token->matched_gherkin_dialect( $self->dialect_name )` which is `$self->dialect_name` which is always interpreted as TRUE.

for me, it is a nightmare and the reason of using implicit return.

I detected this issue when I try to implement an alternate token parser for "Markdown for Gherkin".
created_at: 2025-05-14T08:00:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -274,6 +278,7 @@ sub match_TableRow {
 
     $self->_set_token_matched( $token,
         TableRow => { items => $token->line->table_cells } );
+    return 1;
path: perl/lib/Gherkin/TokenMatcher.pm
user: login: fperrad
body: same answer
created_at: 2025-05-14T08:01:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -51,46 +52,47 @@ sub change_dialect {
                                [ @{ $self->dialect->And }, @{ $self->dialect->But } ],
                                Cucumber::Messages::Step::KEYWORDTYPE_CONJUNCTION);
     $self->_keyword_types( $keyword_types );
+    return;
 }
 
 sub reset {
     my $self = shift;
     $self->change_dialect( $self->_default_dialect_name );
     $self->_indent_to_remove(0);
     $self->_active_doc_string_separator(undef);
-
+    return;
 }
 
 sub match_FeatureLine {
     my ( $self, $token ) = @_;
-    $self->_match_title_line( $token, FeatureLine => $self->dialect->Feature );
+    return $self->_match_title_line( $token, FeatureLine => $self->dialect->Feature );
 }
 
 sub match_RuleLine {
     my ( $self, $token ) = @_;
-    $self->_match_title_line( $token,
+    return $self->_match_title_line( $token,
         RuleLine => $self->dialect->Rule );
 }
 
 sub match_ScenarioLine {
     my ( $self, $token ) = @_;
-    $self->_match_title_line(
+    return $self->_match_title_line(
         $token,
         ScenarioLine => $self->dialect->Scenario )
-        or $self->_match_title_line(
+        || $self->_match_title_line(
path: perl/lib/Gherkin/TokenMatcher.pm
user: login: fperrad
body: For this method, the operator `||` is also used in .NET, Java, JavaScript, PHP and Ruby;
the operator `or` is used in Python.

created_at: 2025-05-14T08:16:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,7 +72,7 @@ sub formatter {
     my $fh   = $self->out_handle;
     return sub {
         my $msg = shift;
-        print $fh $msg->to_json . "\n";
+        print {$fh} $msg->to_json . "\n";
path: perl/lib/App/gherkin.pm
user: login: ehuelsmann
body: That may be the case, but I know of absolutely *no* code base that works that way. So, I asked #perl on libera.chat. They say:

![image](https://github.com/user-attachments/assets/d4d095a8-0008-45e9-9a5b-92149fa94b26)

Please remove this one. It's definitely not idiomatic Perl.
created_at: 2025-05-15T16:04:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -266,6 +269,7 @@ sub _match_DocStringSeparator {
 
     $self->_set_token_matched( $token,
         DocStringSeparator => { text => $content_type, keyword => $separator } );
+    return 1;
path: perl/lib/Gherkin/TokenMatcher.pm
user: login: ehuelsmann
body: > * `return;` interpreted as FALSE (ie. not match)

This is one of those things that #perl in libera.chat refers to which is typically bad practice. Please return an explicit false value (eg., `!!0` or `''`).

Other than that, isn't it correct to return the return value of `set_token_matched`? Because if it's not successful, you're now converting it silently into success by hardcoding the `1` return value?
created_at: 2025-05-15T16:10:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -51,46 +52,47 @@ sub change_dialect {
                                [ @{ $self->dialect->And }, @{ $self->dialect->But } ],
                                Cucumber::Messages::Step::KEYWORDTYPE_CONJUNCTION);
     $self->_keyword_types( $keyword_types );
+    return;
 }
 
 sub reset {
     my $self = shift;
     $self->change_dialect( $self->_default_dialect_name );
     $self->_indent_to_remove(0);
     $self->_active_doc_string_separator(undef);
-
+    return;
 }
 
 sub match_FeatureLine {
     my ( $self, $token ) = @_;
-    $self->_match_title_line( $token, FeatureLine => $self->dialect->Feature );
+    return $self->_match_title_line( $token, FeatureLine => $self->dialect->Feature );
 }
 
 sub match_RuleLine {
     my ( $self, $token ) = @_;
-    $self->_match_title_line( $token,
+    return $self->_match_title_line( $token,
         RuleLine => $self->dialect->Rule );
 }
 
 sub match_ScenarioLine {
     my ( $self, $token ) = @_;
-    $self->_match_title_line(
+    return $self->_match_title_line(
         $token,
         ScenarioLine => $self->dialect->Scenario )
-        or $self->_match_title_line(
+        || $self->_match_title_line(
path: perl/lib/Gherkin/TokenMatcher.pm
user: login: ehuelsmann
body: Ok. the Ruby and Java implementations are the leading implementations. The Python implementation should follow the pattern. So, if the sematic meaning of `||` vs `or` in all of these languages is the same as for Perl, I'll accept that this is an improvement indeed.
created_at: 2025-05-15T16:13:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -51,46 +52,47 @@ sub change_dialect {
                                [ @{ $self->dialect->And }, @{ $self->dialect->But } ],
                                Cucumber::Messages::Step::KEYWORDTYPE_CONJUNCTION);
     $self->_keyword_types( $keyword_types );
+    return;
 }
 
 sub reset {
     my $self = shift;
     $self->change_dialect( $self->_default_dialect_name );
     $self->_indent_to_remove(0);
     $self->_active_doc_string_separator(undef);
-
+    return;
 }
 
 sub match_FeatureLine {
     my ( $self, $token ) = @_;
-    $self->_match_title_line( $token, FeatureLine => $self->dialect->Feature );
+    return $self->_match_title_line( $token, FeatureLine => $self->dialect->Feature );
 }
 
 sub match_RuleLine {
     my ( $self, $token ) = @_;
-    $self->_match_title_line( $token,
+    return $self->_match_title_line( $token,
         RuleLine => $self->dialect->Rule );
 }
 
 sub match_ScenarioLine {
     my ( $self, $token ) = @_;
-    $self->_match_title_line(
+    return $self->_match_title_line(
         $token,
         ScenarioLine => $self->dialect->Scenario )
-        or $self->_match_title_line(
+        || $self->_match_title_line(
path: perl/lib/Gherkin/TokenMatcher.pm
user: login: simcop2387
body: It's all nearly the same, perl has a difference in precedence between `||` and `or` that the other languages don't have but I don't believe it'll come into play in this case.  In perl the `or` operator has a precedence that makes it happen after all other operators so that it doesn't need to be parenthesized for other expressions going on.  That makes it useful for things like:

```
open(my $fh, "<", "...") or die "Failed to open: $1";
```
But here it shouldn't matter either way.
created_at: 2025-05-15T17:26:03Z
author_association: NONE
diff_hunk: @@ -51,46 +52,47 @@ sub change_dialect {
                                [ @{ $self->dialect->And }, @{ $self->dialect->But } ],
                                Cucumber::Messages::Step::KEYWORDTYPE_CONJUNCTION);
     $self->_keyword_types( $keyword_types );
+    return;
 }
 
 sub reset {
     my $self = shift;
     $self->change_dialect( $self->_default_dialect_name );
     $self->_indent_to_remove(0);
     $self->_active_doc_string_separator(undef);
-
+    return;
 }
 
 sub match_FeatureLine {
     my ( $self, $token ) = @_;
-    $self->_match_title_line( $token, FeatureLine => $self->dialect->Feature );
+    return $self->_match_title_line( $token, FeatureLine => $self->dialect->Feature );
 }
 
 sub match_RuleLine {
     my ( $self, $token ) = @_;
-    $self->_match_title_line( $token,
+    return $self->_match_title_line( $token,
         RuleLine => $self->dialect->Rule );
 }
 
 sub match_ScenarioLine {
     my ( $self, $token ) = @_;
-    $self->_match_title_line(
+    return $self->_match_title_line(
         $token,
         ScenarioLine => $self->dialect->Scenario )
-        or $self->_match_title_line(
+        || $self->_match_title_line(
path: perl/lib/Gherkin/TokenMatcher.pm
user: login: ehuelsmann
body: ok. so lets go with what the leading implementations do then. Thanks for jumping in!
created_at: 2025-05-15T18:10:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,7 +72,7 @@ sub formatter {
     my $fh   = $self->out_handle;
     return sub {
         my $msg = shift;
-        print $fh $msg->to_json . "\n";
+        print {$fh} $msg->to_json . "\n";
path: perl/lib/App/gherkin.pm
user: login: fperrad
body: commit removed
created_at: 2025-05-15T20:08:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -266,6 +269,7 @@ sub _match_DocStringSeparator {
 
     $self->_set_token_matched( $token,
         DocStringSeparator => { text => $content_type, keyword => $separator } );
+    return 1;
path: perl/lib/Gherkin/TokenMatcher.pm
user: login: fperrad
body: > Other than that, isn't it correct to return the return value of `set_token_matched`? Because if it's not successful, you're now converting it silently into success by hardcoding the `1` return value?

`_set_token_matched` (or `setTokenMatched` in JavaScript) never fails, it is just a set of member/attribute assignments.
created_at: 2025-05-15T20:29:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -266,6 +269,7 @@ sub _match_DocStringSeparator {
 
     $self->_set_token_matched( $token,
         DocStringSeparator => { text => $content_type, keyword => $separator } );
+    return 1;
path: perl/lib/Gherkin/TokenMatcher.pm
user: login: fperrad
body: > This is one of those things that #perl in libera.chat refers to which is typically bad practice. Please return an explicit false value (eg., `!!0` or `''`).

The current base code have a lot of return nothing. Not in form of `return;`, but like `return unless $token->line->startswith('|');`.
created_at: 2025-05-15T20:32:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -266,6 +269,7 @@ sub _match_DocStringSeparator {
 
     $self->_set_token_matched( $token,
         DocStringSeparator => { text => $content_type, keyword => $separator } );
+    return 1;
path: perl/lib/Gherkin/TokenMatcher.pm
user: login: ehuelsmann
body: > The current base code have a lot of return nothing. Not in form of `return;`, but like `return unless $token->line->startswith('|');`.

Ok, but if we want to improve the code base then returning explicit values is better than doing implicit ones, I would say? similar to how implicit returns are not so good ?  
created_at: 2025-05-15T20:39:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -266,6 +269,7 @@ sub _match_DocStringSeparator {
 
     $self->_set_token_matched( $token,
         DocStringSeparator => { text => $content_type, keyword => $separator } );
+    return 1;
path: perl/lib/Gherkin/TokenMatcher.pm
user: login: ehuelsmann
body: > `_set_token_matched` (or `setTokenMatched` in JavaScript) never fails, it is just a set of member/attribute assignments.

Well, sure. But by hardcoding the `1` return, you're also hard coding knowledge about the implementation of `setTokenMatched`. In general that's not a good idea, because people changing `setTokenMatched` may not be aware that the return value will be discarded later (because the return value is known to be always true, except that after their edits, this is no longer the case).
created_at: 2025-05-15T20:42:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -266,6 +269,7 @@ sub _match_DocStringSeparator {
 
     $self->_set_token_matched( $token,
         DocStringSeparator => { text => $content_type, keyword => $separator } );
+    return 1;
path: perl/lib/Gherkin/TokenMatcher.pm
user: login: fperrad
body: commit removed.
created_at: 2025-05-19T19:17:02Z
author_association: CONTRIBUTOR

PR comments:
user: login: fperrad
created_at: 2025-05-13T16:15:33Z
body: I agree with the goal to code for non-Perl coders.

I push a second version without the following commits:

 - "avoid useless interpolation"
 - "remove useless parentheses for builtin"
 - "update changelog"

From my point of view, the most important commit is "add explicit return".
Because the logic of control flow is not easily understandable.
The implicit return with the value of the last statement is too perlish and causes nightmare.

This serie contains only minor refactors, I don't want introduce big (and risky) refactors at this time.

author_association: CONTRIBUTOR
user: login: ehuelsmann
created_at: 2025-05-13T20:40:57Z
body: Agreed on the three commits you've added there. Please let us know when you're done incorporating our comments by re-requesting review. I'm currently of the assumption there's still work in progress?
author_association: CONTRIBUTOR
user: login: fperrad
created_at: 2025-05-14T15:17:51Z
body: At this time, I've added an answer in each conversation.
author_association: CONTRIBUTOR
user: login: ehuelsmann
created_at: 2025-05-19T22:00:39Z
body: @mpkorstanje with the changelog item removed, can you agree this is an improvement too?
author_association: CONTRIBUTOR
user: login: mpkorstanje
created_at: 2025-05-21T14:58:51Z
body: Cheers!
author_association: CONTRIBUTOR