Skip to content

i18n extract: parseClassName() treats ::class as a class declaration #19579

Description

@Mapiiik

Description

bin/cake i18n extract warns about classes that do not exist. On a stock install the warnings come from CakePHP's own source, which is scanned to build cake.pot:

Could not reflect class/enum Cake\Core\TestSuite\Router in file src/Core/TestSuite/ContainerStubTrait.php: Class "Cake\Core\TestSuite\Router" does not exist
Could not reflect class/enum Cake\Event\true in file src/Event/EventListenerRegistrationTrait.php: Class "Cake\Event\true" does not exist

Cake\Event\true gives it away: a true literal is being read as a class name. Extraction still completes and the output is correct, so this is cosmetic — just noisy, especially in CI.

Cause

I18nExtractCommand::parseClassName() starts looking for a name as soon as it sees T_ENUM or T_CLASS:

if ($type === T_ENUM || $type === T_CLASS) {
    $waitingForName = true;
    continue;
}

if ($waitingForName && $type === T_STRING) {
    return $namespace !== '' ? $namespace . '\\' . $value : $value;
}

But T_CLASS is also the token for the ::class magic constant — Foo::class tokenizes as T_STRING, T_DOUBLE_COLON, T_CLASS. So in is_a($listener, EventListenerInterface::class, true) the parser hits T_CLASS, then takes the next T_STRING, which is true.

The second condition is that both files declare a trait. parseClassName() does not know T_TRAIT, so there is no real declaration to match first and the first ::class in the body wins:

file source parsed as
Event/EventListenerRegistrationTrait.php is_a($listener, EventListenerInterface::class, true) Cake\Event\true
Core/TestSuite/ContainerStubTrait.php class_exists(Router::class) Cake\Core\TestSuite\Router

The second also shows imports are not followed: the file has use Cake\Routing\Router;, but the name is glued to the file's own namespace. When such a guess happens to exist, no warning appears and the wrong class is reflected silently — harmless today only because extractFileReflection() drops anything that is not an enum.

Steps to reproduce

In any 5.4.1 app:

bin/cake i18n extract --extract-core yes --overwrite --paths src/,templates/ --output resources/locales/

--extract-core yes is the part that matters: it puts vendor/cakephp/cakephp/src/ in the scanned paths, and both offending files are there. Nothing in the app itself is needed to trigger it.

Expected

No warnings; a file declaring no class or enum is skipped.

Suggested fix

Track the previous significant token and ignore T_CLASS that follows T_DOUBLE_COLON:

-            if ($type === T_ENUM || $type === T_CLASS) {
+            // `Foo::class` produces T_CLASS as well, and is not a declaration
+            if (($type === T_ENUM || $type === T_CLASS) && $previous !== T_DOUBLE_COLON) {
                 $waitingForName = true;
                 continue;
             }

with $previous assigned on every branch that reaches the end of the loop body.

Running both versions over the two offending files plus a real enum and class:

file current patched
EventListenerRegistrationTrait.php Cake\Event\true null
ContainerStubTrait.php Cake\Core\TestSuite\Router null
an app enum App\Model\Enum\FirstSeenSource unchanged
an app table class App\Model\Table\HistoricalConnectionsTable unchanged

Optionally, class_exists($fqn, false) before new ReflectionClass($fqn) would stop an unresolvable name from becoming an exception at all.

Environment

CakePHP 5.4.1, PHP 8.4.24. Still present on 5.x as of this writing.

CakePHP Version

5.4.1

PHP Version

8.4.24

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions