Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,14 @@ jobs:
cd e2e/bug-15102c
composer install
../../bin/phpstan analyze
- script: |
cd e2e/bug-15184
composer install
# The crash needs OPcache: forked workers switch it off, the --debug main process and
# spawned workers (pcntl_fork disabled, as on Windows) keep it on.
php -r 'if (!extension_loaded("Zend OPcache")) { fwrite(STDERR, "OPcache is not loaded, so this step cannot reproduce the crash.\n"); exit(1); }'
../../bin/phpstan analyze --debug
php -d disable_functions=pcntl_fork ../../bin/phpstan analyze
- script: |
cd e2e/bug-14724
composer install
Expand Down
1 change: 1 addition & 0 deletions compiler/build/scoper.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ function (string $filePath, string $prefix, string $content): string {
function (string $filePath, string $prefix, string $content): string {
if (!in_array($filePath, [
'bin/phpstan',
'src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php',
'src/Testing/TestCaseSourceLocatorFactory.php',
'src/Testing/PHPStanTestCase.php',
'vendor/ondrejmirtes/better-reflection/src/SourceLocator/Type/ComposerSourceLocator.php',
Expand Down
2 changes: 2 additions & 0 deletions e2e/bug-15184/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/vendor/
composer.lock
10 changes: 10 additions & 0 deletions e2e/bug-15184/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"autoload": {
"psr-4": {
"Bug15184\\": "src/"
},
"files": [
"src/Type/optional.php"
]
}
}
11 changes: 11 additions & 0 deletions e2e/bug-15184/phpstan.dist.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
parameters:
level: 8
paths:
- test.php
ignoreErrors:
# The analysis must report the unknown class, not die including src/Type/optional.php again.
-
message: '#^Class Bug15184\\Type\\optional not found\.$#'
identifier: class.notFound
count: 1
path: test.php
9 changes: 9 additions & 0 deletions e2e/bug-15184/src/Type/optional.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php declare(strict_types = 1);

namespace Bug15184\Type;

// One function per file, loaded through Composer's "files" autoload, in a directory
// the PSR-4 prefix also maps - the layout of php-standard-library and azjezz/psl.
function optional(): void
{
}
8 changes: 8 additions & 0 deletions e2e/bug-15184/test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php declare(strict_types = 1);

namespace Bug15184;

function check(object $o): bool
{
return $o instanceof \Bug15184\Type\optional;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Reflection\BetterReflection\SourceLocator;

use Composer\Autoload\ClassLoader;
use Override;
use ParseError;
use PhpParser\Node\Arg;
Expand Down Expand Up @@ -31,12 +32,15 @@
use function defined;
use function function_exists;
use function interface_exists;
use function is_array;
use function is_file;
use function is_string;
use function opcache_invalidate;
use function realpath;
use function restore_error_handler;
use function set_error_handler;
use function spl_autoload_functions;
use function str_contains;
use function strtolower;
use function trait_exists;
use const PHP_VERSION_ID;
Expand Down Expand Up @@ -336,14 +340,21 @@ private function locateClassByName(string $className): ?array
$this->silenceErrors();

try {
$foundByComposer = false;
$result = FileReadTrapStreamWrapper::withStreamWrapperOverride(
static function () use ($className): ?array {
static function () use ($className, &$foundByComposer): ?array {
$functions = spl_autoload_functions();
if ($functions === false) {
return null;
}

foreach ($functions as $preExistingAutoloader) {
$composerFile = self::findFileWithComposer($preExistingAutoloader, $className);
if ($composerFile !== null) {
$foundByComposer = true;
return [[$composerFile], $className, null];
}

try {
$preExistingAutoloader($className);
} catch (ParseError) {
Expand All @@ -370,7 +381,8 @@ static function () use ($className): ?array {
return null;
}

if (!function_exists('opcache_invalidate')) {
// nothing was included, so there is no trapped script to invalidate
if ($foundByComposer || !function_exists('opcache_invalidate')) {
return $result;
}

Expand All @@ -387,6 +399,39 @@ static function () use ($className): ?array {
}
}

/**
* Asks Composer's ClassLoader which file loadClass() would include, without
* including it. With OPcache holding a file this process already ran, an
* include inside the trap runs the cached script instead of the trap's empty
* one, and a file declaring a function dies with "Cannot redeclare".
*
* A path behind a stream wrapper is left to the trap: realpath() cannot
* resolve it, and the phar's own ClassLoader maps PHPStan\ to phar://.
*
* @param callable(string): void $autoloader
*/
private static function findFileWithComposer(callable $autoloader, string $className): ?string
{
if (!is_array($autoloader) || !$autoloader[0] instanceof ClassLoader) {
return null;
}

$file = $autoloader[0]->findFile($className);
if ($file === false || str_contains($file, '://')) {
return null;
}

// findFile() can return ../ segments and mixed separators; resolve them
// as an include would, so that locateIdentifier() can compare the path
// with ReflectionClass::getFileName()
$resolvedFile = realpath($file);
if ($resolvedFile === false || !is_file($resolvedFile)) {
return null;
}

return $resolvedFile;
}

private function silenceErrors(): void
{
set_error_handler(static fn (): bool => true);
Expand Down
Loading