Repository: Behat/Gherkin (php)

Issue:
Missing feature keyword should not create feature
Cucumber insists on the Feature being present but we don't.

Removing this line will add a failing test
https://github.com/Behat/Gherkin/blob/master/tests/Behat/Gherkin/Cucumber/CompatibilityTest.php#L37

Pull request description:
change: Do not throw on `.feature` file that does not contain a Feature
For parity with cucumber/gherkin - if there is no Feature tag then we
may get an empty line before the final EOS, which we should ignore.

Also adds a cucumber parity test that is now passing, and updates the
reason why another is still failing.

Fixes #208

Patch:
diff --git a/phpstan.dist.neon b/phpstan.dist.neon
index 46b0f47..81206c1 100644
--- a/phpstan.dist.neon
+++ b/phpstan.dist.neon
@@ -1,4 +1,5 @@
 parameters:
+    treatPhpDocTypesAsCertain: false
     level: 5
     paths:
         - src
diff --git a/src/Node/ExampleNode.php b/src/Node/ExampleNode.php
index 8a0f8db..c524935 100644
--- a/src/Node/ExampleNode.php
+++ b/src/Node/ExampleNode.php
@@ -198,14 +198,16 @@ class ExampleNode implements ScenarioInterface, NamedScenarioInterface
      */
     protected function replaceTableArgumentTokens(TableNode $argument)
     {
-        $table = $argument->getTable();
-        foreach ($table as $line => $row) {
-            foreach (array_keys($row) as $col) {
-                $table[$line][$col] = $this->replaceTextTokens($table[$line][$col]);
+        $replacedTable = [];
+        foreach ($argument->getTable() as $line => $row) {
+            $replacedRow = [];
+            foreach ($row as $value) {
+                $replacedRow[] = $this->replaceTextTokens($value);
             }
+            $replacedTable[$line] = $replacedRow;
         }
 
-        return new TableNode($table);
+        return new TableNode($replacedTable);
     }
 
     /**
diff --git a/src/Node/TableNode.php b/src/Node/TableNode.php
index ea7612d..bcd4d3f 100644
--- a/src/Node/TableNode.php
+++ b/src/Node/TableNode.php
@@ -20,6 +20,8 @@ use ReturnTypeWillChange;
  * Represents Gherkin Table argument.
  *
  * @author Konstantin Kudryashov <ever.zet@gmail.com>
+ *
+ * @template-implements IteratorAggregate<int, array<string, string>>
  */
 class TableNode implements ArgumentInterface, IteratorAggregate
 {
@@ -31,7 +33,7 @@ class TableNode implements ArgumentInterface, IteratorAggregate
     /**
      * Initializes table.
      *
-     * @param array<int, array> $table Table in form of [$rowLineNumber => [$val1, $val2, $val3]]
+     * @param array<int, list<string>> $table Table in form of [$rowLineNumber => [$val1, $val2, $val3]]
      *
      * @throws NodeException If the given table is invalid
      */
@@ -84,7 +86,7 @@ class TableNode implements ArgumentInterface, IteratorAggregate
     /**
      * Creates a table from a given list.
      *
-     * @param array $list One-dimensional array
+     * @param array<int, string> $list One-dimensional array
      *
      * @return TableNode
      *
@@ -96,11 +98,9 @@ class TableNode implements ArgumentInterface, IteratorAggregate
             throw new NodeException('List is not a one-dimensional array.');
         }
 
-        array_walk($list, function (&$item) {
-            $item = [$item];
-        });
+        $table = array_map(fn ($item) => [$item], $list);
 
-        return new self($list);
+        return new self($table);
     }
 
     /**
@@ -116,7 +116,7 @@ class TableNode implements ArgumentInterface, IteratorAggregate
     /**
      * Returns table hash, formed by columns (ColumnsHash).
      *
-     * @return array
+     * @return list<array<string, string>>
      */
     public function getHash()
     {
@@ -126,7 +126,7 @@ class TableNode implements ArgumentInterface, IteratorAggregate
     /**
      * Returns table hash, formed by columns.
      *
-     * @return array
+     * @return list<array<string, string>>
      */
     public function getColumnsHash()
     {
@@ -135,6 +135,7 @@ class TableNode implements ArgumentInterface, IteratorAggregate
 
         $hash = [];
         foreach ($rows as $row) {
+            \assert($keys !== null); // If there is no first row due to an empty table, we won't enter this loop either.
             $hash[] = array_combine($keys, $row);
         }
 
@@ -144,7 +145,7 @@ class TableNode implements ArgumentInterface, IteratorAggregate
     /**
      * Returns table hash, formed by rows.
      *
-     * @return array
+     * @return array<string, string|list<string>>
      */
     public function getRowsHash()
     {
@@ -161,7 +162,7 @@ class TableNode implements ArgumentInterface, IteratorAggregate
      * Returns numerated table lines.
      * Line numbers are keys, lines are values.
      *
-     * @return array
+     * @return array<int, list<string>>
      */
     public function getTable()
     {
@@ -171,7 +172,7 @@ class TableNode implements ArgumentInterface, IteratorAggregate
     /**
      * Returns table rows.
      *
-     * @return array
+     * @return list<list<string>>
      */
     public function getRows()
     {
@@ -193,7 +194,7 @@ class TableNode implements ArgumentInterface, IteratorAggregate
      *
      * @param int $index Row number
      *
-     * @return array
+     * @return list<string>
      *
      * @throws NodeException If row with specified index does not exist
      */
@@ -213,7 +214,7 @@ class TableNode implements ArgumentInterface, IteratorAggregate
      *
      * @param int $index Column number
      *
-     * @return array
+     * @return list<string>
      *
      * @throws NodeException If column with specified index does not exist
      */
diff --git a/src/Parser.php b/src/Parser.php
index 1c38dc3..5537a9c 100644
--- a/src/Parser.php
+++ b/src/Parser.php
@@ -82,7 +82,7 @@ class Parser
         while ($this->predictTokenType() !== 'EOS') {
             $node = $this->parseExpression();
 
-            if ($node === "\n") {
+            if ($node === "\n" || $node === '') {
                 continue;
             }
 


Test patch:
diff --git a/tests/Cucumber/CompatibilityTest.php b/tests/Cucumber/CompatibilityTest.php
index dc9c804..1259a41 100644
--- a/tests/Cucumber/CompatibilityTest.php
+++ b/tests/Cucumber/CompatibilityTest.php
@@ -51,10 +51,8 @@ class CompatibilityTest extends TestCase
         'feature_keyword_in_scenario_description.feature' => 'Scenario descriptions not supported',
         'padded_example.feature' => 'Table padding is not trimmed as aggressively',
         'spaces_in_language.feature' => 'Whitespace not supported around language selector',
-        'incomplete_feature_3.feature' => 'file with no feature keyword not handled correctly',
         'rule_without_name_and_description.feature' => 'Rule is wrongly parsed as Description',
-        'incomplete_scenario.feature' => 'Wrong background parsing when there are no steps',
-        'incomplete_background_2.feature' => 'Wrong background parsing when there are no steps',
+        'incomplete_background_2.feature' => 'Background descriptions not supported',
     ];
 
     /**
diff --git a/tests/Node/TableNodeTest.php b/tests/Node/TableNodeTest.php
index 5002769..3d1321b 100644
--- a/tests/Node/TableNodeTest.php
+++ b/tests/Node/TableNodeTest.php
@@ -47,6 +47,7 @@ class TableNodeTest extends TestCase
             new NodeException("Table cell at row '0', col '0' is expected to be scalar, got array")
         );
 
+        // @phpstan-ignore argument.type (we are explicitly testing an invalid instantiation)
         new TableNode([
             [['everzet', 'antono']],
         ]);
@@ -279,6 +280,20 @@ class TableNodeTest extends TestCase
         $this->assertEquals($expected, $table);
     }
 
+    public function testFromListWithLineNumbers(): void
+    {
+        $table = TableNode::fromList([
+            12 => 'everzet',
+            15 => 'antono',
+        ]);
+
+        $expected = new TableNode([
+            12 => ['everzet'],
+            15 => ['antono'],
+        ]);
+        $this->assertEquals($expected, $table);
+    }
+
     public function testMergeRowsFromTablePassSeveralTablesShouldBeMerged(): void
     {
         $table = new TableNode([
@@ -333,9 +348,10 @@ class TableNodeTest extends TestCase
             new NodeException('List is not a one-dimensional array.')
         );
 
+        // @phpstan-ignore argument.type (we are explicitly testing an invalid instantiation)
         TableNode::fromList([
-            [1, 2, 3],
-            [4, 5, 6],
+            ['1', '2', '3'],
+            ['4', '5', '6'],
         ]);
     }