Skip to content

System error: "'\self' is an invalid class name40 after upgrading #9493

Description

@devnix

Bug Report

Subject Details
Rector version where I was upgrading from 2.2.3
Rector version where the issue was found 2.2.7
Rector version where I'm pinpointing the error 2.2.5

After upgrading to this version, I'm getting a big bunch of errors after processing some files, I've uploaded them as an attachment.

rector bug.txt

Minimal PHP Code Causing Issue

It's a propietary codebase, but I casually found an open source script that reports the exact same error:

<?php

namespace Grav\Plugin;

use Grav\Common\Cache;
use Grav\Common\Plugin;
use Grav\Common\Uri;

class ProblemsPlugin extends Plugin
{
    protected $results = [];

    protected $check;

    /**
     * @return array
     */
    public static function getSubscribedEvents()
    {
        return [
            'onPluginsInitialized' => ['onPluginsInitialized', 100001],
            'onFatalException' => ['onFatalException', 0],
        ];
    }

    public function onFatalException()
    {
        if ($this->isAdmin()) {
            $this->active = false;

            return;
        }

        // Run through potential issues
        if ($this->problemChecker()) {
            $this->renderProblems();
        }
    }

    public function onPluginsInitialized()
    {
        if ($this->isAdmin()) {
            $this->active = false;

            return;
        }

        /** @var Cache $cache */
        $cache = $this->grav['cache'];
        $validated_prefix = 'problem-check-';

        $this->check = CACHE_DIR.$validated_prefix.$cache->getKey();

        if (!file_exists($this->check)) {
            // If no issues remain, save a state file in the cache
            if (!$this->problemChecker()) {
                // delete any existing validated files
                foreach (new \GlobIterator(CACHE_DIR.$validated_prefix.'*') as $fileInfo) {
                    @unlink($fileInfo->getPathname());
                }

                // create a file in the cache dir so it only runs on cache changes
                touch($this->check);
            } else {
                $this->renderProblems();
            }
        }
    }

    protected function renderProblems()
    {
        $theme = 'antimatter';

        /** @var Uri $uri */
        $uri = $this->grav['uri'];
        $baseUrlRelative = $uri->rootUrl(false);
        $themeUrl = $baseUrlRelative.'/'.USER_PATH.basename(THEMES_DIR).'/'.$theme;
        $problemsUrl = $baseUrlRelative.'/user/plugins/problems';

        $html = file_get_contents(__DIR__.'/html/problems.html');

        /**
         * Process the results, ignore the statuses passed as $ignore_status.
         *
         * @param $results
         * @param $ignore_status
         */
        $processResults = function ($results, $ignore_status) {
            $problems = '';

            foreach ($results as $key => $result) {
                if ('files' == $key || 'apache' == $key || 'execute' == $key) {
                    foreach ($result as $key_text => $value_text) {
                        foreach ($value_text as $status => $text) {
                            if ($status == $ignore_status) {
                                continue;
                            }
                            $problems .= $this->getListRow($status, '<b>'.$key_text.'</b> '.$text);
                        }
                    }
                } else {
                    foreach ($result as $status => $text) {
                        if ($status == $ignore_status) {
                            continue;
                        }
                        $problems .= $this->getListRow($status, $text);
                    }
                }
            }

            return $problems;
        };

        // First render the errors
        $problems = $processResults($this->results, 'success');

        // Then render the successful checks
        $problems .= $processResults($this->results, 'error');

        $html = str_replace('%%BASE_URL%%', $baseUrlRelative, $html);
        $html = str_replace('%%THEME_URL%%', $themeUrl, $html);
        $html = str_replace('%%PROBLEMS_URL%%', $problemsUrl, $html);
        $html = str_replace('%%PROBLEMS%%', $problems, $html);

        echo $html;
        http_response_code(500);

        exit;
    }

    protected function getListRow($status, $text)
    {
        if ('error' == $status) {
            $icon = 'fa-times';
        } elseif ('info' == $status) {
            $icon = 'fa-info';
        } else {
            $icon = 'fa-check';
        }
        $output = "\n";
        $output .= '<li class="'.$status.' clearfix"><i class="fa fa-fw '.$icon.'"></i><p>'.$text.'</p></li>';

        return $output;
    }

    protected function problemChecker()
    {
        $min_php_version = \defined('GRAV_PHP_MIN') ? GRAV_PHP_MIN : '5.4.0';
        $problems_found = false;

        $essential_files = [
            'cache' => true,
            'logs' => true,
            'images' => true,
            'assets' => true,
            'system' => false,
            'user/data' => true,
            'user/pages' => false,
            'user/config' => false,
            'user/plugins/error' => false,
            'user/plugins' => false,
            'user/themes' => false,
            'vendor' => false,
        ];

        if (version_compare(GRAV_VERSION, '0.9.27', '>=')) {
            $essential_files['backup'] = true;
            $backup_folder = ROOT_DIR.'backup';
            // try to create backup folder if missing
            if (!file_exists($backup_folder)) {
                @mkdir($backup_folder, 0770);
            }
        }

        if (version_compare(GRAV_VERSION, '1.1.4', '>=')) {
            $essential_files['tmp'] = true;
            $tmp_folder = ROOT_DIR.'tmp';
            // try to create tmp folder if missing
            if (!file_exists($tmp_folder)) {
                @mkdir($tmp_folder, 0770);
            }
        }

        // Perform some Apache checks
        if (str_contains(php_sapi_name(), 'apache')) {
            $require_apache_modules = ['mod_rewrite'];
            $apache_modules = apache_get_modules();

            $apache_status = [];

            foreach ($require_apache_modules as $module) {
                if (\in_array($module, $apache_modules)) {
                    $apache_module_adjective = ' Apache module is enabled';
                    $apache_module_status = 'success';
                } else {
                    $problems_found = true;
                    $apache_module_adjective = ' Apache module is not installed or enabled';
                    $apache_module_status = 'error';
                }
                $apache_status[$module] = [$apache_module_status => $apache_module_adjective];
            }

            if (\sizeof($apache_status) > 0) {
                $this->results['apache'] = $apache_status;
            }
        }

        // Check PHP version
        if (version_compare(phpversion(), $min_php_version, '<')) {
            $problems_found = true;
            $php_version_adjective = 'lower';
            $php_version_status = 'error';
        } else {
            $php_version_adjective = 'greater';
            $php_version_status = 'success';
        }
        $this->results['php'] = [$php_version_status => 'Your PHP version ('.phpversion().') is '.$php_version_adjective.' than the minimum required: <b>'.$min_php_version.'</b>  - <a href="http://getgrav.org/blog/changing-php-requirements-to-5.5">Additional Information</a>'];

        // Check for GD library
        if (\defined('GD_VERSION') && \function_exists('gd_info')) {
            $gd_adjective = '';
            $gd_status = 'success';
        } else {
            $problems_found = true;
            $gd_adjective = 'not ';
            $gd_status = 'error';
        }
        $this->results['gd'] = [$gd_status => 'PHP GD (Image Manipulation Library) is '.$gd_adjective.'installed'];

        // Check for PHP CURL library
        if (\function_exists('curl_version')) {
            $curl_adjective = '';
            $curl_status = 'success';
        } else {
            $problems_found = true;
            $curl_adjective = 'not ';
            $curl_status = 'error';
        }
        $this->results['curl'] = [$curl_status => 'PHP Curl (Data Transfer Library) is '.$curl_adjective.'installed'];

        // Check for PHP Open SSL library
        if (\extension_loaded('openssl') && \defined('OPENSSL_VERSION_TEXT')) {
            $ssl_adjective = '';
            $ssl_status = 'success';
        } else {
            $problems_found = true;
            $ssl_adjective = 'not ';
            $ssl_status = 'error';
        }
        $this->results['ssl'] = [$ssl_status => 'PHP OpenSSL (Secure Sockets Library) is '.$ssl_adjective.'installed'];

        // Check for PHP XML library
        if (\extension_loaded('xml')) {
            $xml_adjective = '';
            $xml_status = 'success';
        } else {
            $problems_found = true;
            $xml_adjective = 'not ';
            $xml_status = 'error';
        }
        $this->results['xml'] = [$xml_status => 'PHP XML Library is '.$xml_adjective.'installed'];

        // Check for PHP MbString library
        if (\extension_loaded('mbstring')) {
            $mbstring_adjective = '';
            $mbstring_status = 'success';
        } else {
            $problems_found = true;
            $mbstring_adjective = 'not ';
            $mbstring_status = 'error';
        }
        $this->results['mbstring'] = [$mbstring_status => 'PHP Mbstring (Multibyte String Library) is '.$mbstring_adjective.'installed'];

        // Check Exif if enabled
        if ($this->grav['config']->get('system.media.auto_metadata_exif')) {
            if (\extension_loaded('exif')) {
                $exif_adjective = '';
                $exif_status = 'success';
            } else {
                $problems_found = true;
                $exif_adjective = 'not ';
                $exif_status = 'error';
            }
            $this->results['exif'] = [$exif_status => 'PHP Exif (Exchangeable Image File Format) is '.$exif_adjective.'installed'];
        }

        // Check for PHP Zip library
        if (\extension_loaded('zip')) {
            $zip_adjective = '';
            $zip_status = 'success';
        } else {
            $problems_found = true;
            $zip_adjective = 'not ';
            $zip_status = 'error';
        }
        $this->results['zip'] = [$zip_status => 'PHP Zip extension is '.$zip_adjective.'installed'];

        // Check for essential files & perms
        $file_problems = [];
        foreach ($essential_files as $file => $check_writable) {
            $file_path = ROOT_DIR.$file;
            $is_dir = false;
            if (!file_exists($file_path)) {
                $problems_found = true;
                $file_status = 'error';
                $file_adjective = 'does not exist';
            } else {
                $file_status = 'success';
                $file_adjective = 'exists';
                $is_writeable = is_writable($file_path);
                $is_dir = is_dir($file_path);

                if ($check_writable) {
                    if (!$is_writeable) {
                        $file_status = 'error';
                        $problems_found = true;
                        $file_adjective .= ' but is <b class="underline">not writeable</b>';
                    } else {
                        $file_adjective .= ' and <b class="underline">is writeable</b>';
                    }
                }
            }

            $file_problems[$file_path] = [$file_status => $file_adjective];
        }
        if (\sizeof($file_problems) > 0) {
            $this->results['files'] = $file_problems;
        }

        return $problems_found;
    }
}

And here's our current configuration, it's a bit messy as we are enabling rules one by one:

<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\SetList;

return RectorConfig::configure()
    // ->withParallel()
    ->withCache(__DIR__.'/var/cache/rector')
    ->withPaths([
        __DIR__ . '/inc',
        __DIR__ . '/migrations',
        __DIR__ . '/public',
        __DIR__ . '/src',
        __DIR__ . '/tests',
        __DIR__ . '/utils',
    ])
    ->withPHPStanConfigs([
        __DIR__ . '/phpstan.dist.neon',
    ])
    ->withRules([
        // Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector::class,
        // Utils\Rector\Rector\LegacyModelArrayAccessToGetterRector::class,
        Utils\Rector\Rector\LegacyModelGetSportEventIdRector::class,
    ])

    // ->withPhpSets()
    ->withTypeCoverageLevel(6)
    ->withDeadCodeLevel(1)
    // ->withCodeQualityLevel(0)
    // ->withCodingStyleLevel(0)
    ->withSets([
        SetList::PHP_81,
    ])
    ->withComposerBased(
        twig: true,
        doctrine: true,
        phpunit: true,
        symfony: true,
    )
    ->withAttributesSets(
        symfony: false,
        doctrine: true,
        phpunit: false,
        fosRest: false,
        jms: false,
        sensiolabs: false,
        all: false,
        symfonyRoute: false,
        symfonyValidator: false,
    )
    ->withPreparedSets(
        deadCode: false,
        codeQuality: false,
        codingStyle: false,
        typeDeclarations: false,
        typeDeclarationDocblocks: false,
        privatization: false,
        naming: false,
        instanceOf: false,
        earlyReturn: false,
        strictBooleans: false,
        carbon: false,
        rectorPreset: false,
        // phpunitCodeQuality: true,
        doctrineCodeQuality: true,
        // symfonyCodeQuality: true,
        symfonyConfigs: true,
    )
    ->withSkip([
        // \Rector\TypeDeclaration\Rector\ClassMethod\ReturnNeverTypeRector::class,
        // \Rector\Php81\Rector\Class_\MyCLabsClassToEnumRector::class,
        // \Rector\Php81\Rector\MethodCall\MyCLabsMethodCallToEnumConstRector::class,
        // \Rector\Php81\Rector\New_\MyCLabsConstructorCallToEnumFromRector::class,
        \Rector\Php81\Rector\Property\ReadOnlyPropertyRector::class,
        \Rector\Php81\Rector\Class_\SpatieEnumClassToEnumRector::class,
        \Rector\Php81\Rector\MethodCall\SpatieEnumMethodCallToEnumConstRector::class,
        \Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector::class,
        \Rector\Php81\Rector\Array_\FirstClassCallableRector::class,
        \Rector\Php81\Rector\MethodCall\RemoveReflectionSetAccessibleCallsRector::class,
    ])
;

Expected Behaviour

No errors 😄

Activity

  1. samsonasik commented on Nov 15, 2025

    @samsonasik
    Member

    Could you create reproducible error at https://getrector.com/demo ? Thank you.

  2. devnix commented on Nov 15, 2025

    @devnix
    Author

    I've not been able to reproduce it within the 100 lines limit. Creating a local reproducer with this file isolated doesn't seem to report any error :/

  3. samsonasik commented on Nov 15, 2025

    @samsonasik
    Member

    How about create simple reproducible repository we can clone and verify?

  4. devnix commented on Nov 15, 2025

    @devnix
    Author

    That's what I've tried to do but the error in this file isolated is not getting replicated, and the codebase that's failing is proprietary.

    I will keep trying to pinpoint the problem to different files in isolation in order to create a reproducer that I can work on in order to be shareable, but I fear that's going to be a complex problem involving rules applied to other files.

  5. devnix commented on Nov 16, 2025

    @devnix
    Author

    It's very hard to track it down. If I start skipping directories to isolate the files that report this error it works. I'm disabling rulesets one by one to see if I can narrow the problem to a specific rule.

  6. devnix commented on Nov 17, 2025

    @devnix
    Author

    I was wondering, is there any other way to get a better stacktrace?

  7. samsonasik commented on Nov 17, 2025

    @samsonasik
    Member

    If it goes to deep phpstan.phar, that seems hard to track unfortunatelly

  8. devnix commented on Nov 17, 2025

    @devnix
    Author

    After several attempts of running rector from source and load phpstan from source too and not getting a good stacktrace, I've attempted this, since I felt like the original exception is being masked:

    Image

    This is the first part of the dumped error:

    object(PhpParser\Error)#147283 (9) {
      ["message":protected]=>
      string(43) "'\self' is an invalid class name on line 40"
      ["string":"Exception":private]=>
      string(0) ""
      ["code":protected]=>
      int(0)
      ["file":protected]=>
      string(88) "/home/devnix/dev/rector/rector-src/vendor/nikic/php-parser/lib/PhpParser/NameContext.php"
      ["line":protected]=>
      int(105)
      ["trace":"Exception":private]=>
    

    and this is the dumped node

    object(PhpParser\Node\Name\FullyQualified)#4601 (2) {
      ["attributes":protected]=>
      array(6) {
        ["startLine"]=>
        int(40)
        ["startTokenPos"]=>
        int(195)
        ["startFilePos"]=>
        int(1298)
        ["endLine"]=>
        int(40)
        ["endTokenPos"]=>
        int(195)
        ["endFilePos"]=>
        int(1302)
      }
      ["name"]=>
      string(4) "self"
    }
    

    and happened while scanning a specific file, which contains something like

    set_exception_handler(\self::custom_exception_handler(...));

    at the line 40, after performing a conversion from

    set_exception_handler(Closure::fromCallable([self::class, 'custom_exception_handler']));

    I guess here could be a reproducer, reporting a more understandable error: https://getrector.com/demo/c661b304-59b3-4bdf-b9c8-29b473cc8754

    It looks indeed like it's not valid PHP: https://3v4l.org/lR7Qg

    Removing the \ character leads to a correct Rector output!

  9. samsonasik commented on Nov 17, 2025

    @samsonasik
    Member

    That seems already error in php level, so rector can't handle it since php-parser confirm, I think using search -> replace with IDE will make it works :)

  10. samsonasik commented on Nov 17, 2025

    @samsonasik
    Member

    I am going to close it, as it already error too early before it scanned per your provided test https://3v4l.org/lR7Qg

    I think you can use php-parallel-lint tool to verify your code base first https://github.com/php-parallel-lint/PHP-Parallel-Lint

    Image

    Thank you for understanding.

  11. devnix commented on Nov 17, 2025

    @devnix
    Author

    \self was generated by the ClosureFromCallableToFirstClassCallableRector rule.

    Looking at your fix at rectorphp/rector-phpunit#587, I've created a reproducer and fixed that specific case: #7632

  12. github-actions commented on Apr 19, 2026

    @github-actions
    Contributor

    This issue has been automatically locked because it has been closed for 150 days. Please open a new issue if you have a similar problem.

  13. locked as resolved and limited conversation to collaborators on Apr 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions