Repository: Codeception/Codeception (php)

Issue:
"Warning: Uninitialized string offset 1 in Parser.php:151" when using Attributes on anonymous classes
#### What are you trying to achieve?

In some of our testcases we use anonymous classes. When adding an attribute to this class it results in a warning thrown by codeception. The tests run fine though.

#### What do you get instead?

```bash
 vendor/bin/codecept run Unit --xml=test-reports/phpunit.xml
Codeception PHP Testing Framework v5.0.6 https://helpukrainewin.org

Warning: Uninitialized string offset 1 in /var/www/html/vendor/codeception/codeception/src/Codeception/Lib/Parser.php on line 151
```

> Provide test source code if related

Can be reproduced with this test:

```php
<?php

namespace Tests\Unit;

use JetBrains\PhpStorm\Immutable;
use Tests\Support\UnitTester;

class WarningTestCest
{
    // tests
    public function tryToTest(UnitTester $I)
    {
        //Here the attribute is added, this produces the warning
        $class = new #[Immutable] class {
            public function foo(): string
            {
                return 'test';
            }
        };

        $I->assertEquals('test', $class->foo());
    }
}
```
### Details

* Codeception version: 4 and 5
* PHP Version: 8.1


Pull request:
Improved handling of anonymous classes in parser
Fixes #6625

Patch:
diff --git a/src/Codeception/Lib/Parser.php b/src/Codeception/Lib/Parser.php
index cf27017cf..4c91a59d7 100644
--- a/src/Codeception/Lib/Parser.php
+++ b/src/Codeception/Lib/Parser.php
@@ -113,11 +113,8 @@ public static function getClassesFromFile(string $file): array
     {
         $sourceCode = file_get_contents($file);
         $classes    = [];
-        if (PHP_MAJOR_VERSION > 5) {
-            $tokens = token_get_all($sourceCode, TOKEN_PARSE);
-        } else {
-            $tokens = token_get_all($sourceCode);
-        }
+        $tokens = token_get_all($sourceCode, TOKEN_PARSE);
+
         $tokenCount = count($tokens);
         $namespace = '';
 
@@ -135,19 +132,31 @@ public static function getClassesFromFile(string $file): array
             }
 
             if ($tokens[$i][0] === T_CLASS) {
+                // class at the beginning of file
                 if (!isset($tokens[$i - 2])) {
                     $classes[] = $namespace . $tokens[$i + 2][1];
                     continue;
                 }
+                // new class
                 if ($tokens[$i - 2][0] === T_NEW) {
                     continue;
                 }
+                // :: class
                 if ($tokens[$i - 1][0] === T_WHITESPACE && $tokens[$i - 2][0] === T_DOUBLE_COLON) {
                     continue;
                 }
+                // ::class
                 if ($tokens[$i - 1][0] === T_DOUBLE_COLON) {
                     continue;
                 }
+                // class{
+                if (isset($tokens[$i + 1]) && ($tokens[$i + 1] === '{')) {
+                    continue;
+                }
+                // class {
+                if (isset($tokens[$i + 2]) && $tokens[$i + 1][0] === T_WHITESPACE && $tokens[$i + 2] === '{') {
+                    continue;
+                }
                 $classes[] = $namespace . $tokens[$i + 2][1];
             }
         }


Test patch:
diff --git a/tests/data/AnonymousClassWithAttributeCest.php b/tests/data/AnonymousClassWithAttributeCest.php
new file mode 100644
index 000000000..7b4cecbdd
--- /dev/null
+++ b/tests/data/AnonymousClassWithAttributeCest.php
@@ -0,0 +1,34 @@
+<?php
+// phpcs:ignoreFile
+namespace Tests\Unit;
+
+use JetBrains\PhpStorm\Immutable;
+use Tests\Support\UnitTester;
+
+class AnonymousClassWithAttributeCest
+{
+    public function tryToTest(UnitTester $I)
+    {
+        $class1 = new #[Immutable] class{
+            public function foo(): string
+            {
+                return 'test';
+            }
+        };
+
+        $class2 = new #[Immutable] class {
+            public function foo(): string
+            {
+                return 'test';
+            }
+        };
+
+        $class3 = new #[Immutable] class
+        {
+            public function foo(): string
+            {
+                return 'test';
+            }
+        };
+    }
+}
diff --git a/tests/unit/Codeception/Lib/ParserTest.php b/tests/unit/Codeception/Lib/ParserTest.php
index 83ff2c5c1..4e86a9a6f 100644
--- a/tests/unit/Codeception/Lib/ParserTest.php
+++ b/tests/unit/Codeception/Lib/ParserTest.php
@@ -169,4 +169,11 @@ public function testNamedParameterNamedClassIsNotClass()
         $classes = Parser::getClassesFromFile(codecept_data_dir('namedParameter.php'));
         $this->assertEquals([], $classes);
     }
+
+    #[Group('core')]
+    public function testParseTestContainingAnnonymousClassWithAttribute()
+    {
+        $classes = Parser::getClassesFromFile(codecept_data_dir('AnonymousClassWithAttributeCest.php'));
+        $this->assertEquals(['Tests\Unit\AnonymousClassWithAttributeCest'], $classes);
+    }
 }