Skip to content

Commit

Permalink
Use AutoloadSourceLocator to see existing classes in memory for stati…
Browse files Browse the repository at this point in the history
…c reflection
  • Loading branch information
ondrejmirtes committed Feb 9, 2020
1 parent 775aee8 commit c0cb3a6
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 6 deletions.
2 changes: 0 additions & 2 deletions build/phpstan.static-php-parser.neon
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,3 @@ includes:
parameters:
featureToggles:
staticReflectionForPhpParser: true
excludes_analyse:
- ../tests/PHPStan/Command/IgnoredRegexValidatorTest.php
1 change: 1 addition & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,7 @@ services:
reflectionProvider: @betterReflectionProvider
patterns:
- '#^PhpParser\\#'
- '#^PHPStan\\#'
- '#^Hoa\\#'
autowired: false

Expand Down
6 changes: 6 additions & 0 deletions phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@
<property name="spacingAfterLast" value="1"/>
</properties>
</rule>
<rule ref="PSR1.Methods.CamelCapsMethodName.NotCamelCaps">
<exclude-pattern>src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php</exclude-pattern>
</rule>
<rule ref="Consistence.NamingConventions.ValidVariableName.NotCamelCaps">
<exclude-pattern>src/Reflection/BetterReflection/SourceLocator/AutoloadSourceLocator.php</exclude-pattern>
</rule>
<exclude-pattern>tests/*/data</exclude-pattern>
<exclude-pattern>tests/*/traits</exclude-pattern>
<exclude-pattern>tests/tmp</exclude-pattern>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Reflection\BetterReflection;

use PHPStan\DependencyInjection\Container;
use PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator;
use PHPStan\Reflection\BetterReflection\SourceLocator\ComposerJsonAndInstalledJsonSourceLocatorMaker;
use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedDirectorySourceLocatorRepository;
use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedSingleFileSourceLocatorRepository;
Expand Down Expand Up @@ -133,6 +134,7 @@ public function create(): SourceLocator
$astLocator = new Locator($this->parser, function (): FunctionReflector {
return $this->container->getService('betterReflectionFunctionReflector');
});
$locators[] = new AutoloadSourceLocator($astLocator);
$locators[] = new PhpInternalSourceLocator($astLocator, $this->phpStormStubsSourceStubber);

return new MemoizingSourceLocator(new AggregateSourceLocator($locators));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
<?php declare(strict_types = 1);

namespace PHPStan\Reflection\BetterReflection\SourceLocator;

use PHPStan\File\FileReader;
use ReflectionClass;
use ReflectionException;
use Roave\BetterReflection\Identifier\Identifier;
use Roave\BetterReflection\Identifier\IdentifierType;
use Roave\BetterReflection\Reflection\Reflection;
use Roave\BetterReflection\Reflector\Exception\IdentifierNotFound;
use Roave\BetterReflection\Reflector\Reflector;
use Roave\BetterReflection\SourceLocator\Ast\Exception\ParseToAstFailure;
use Roave\BetterReflection\SourceLocator\Ast\Locator as AstLocator;
use Roave\BetterReflection\SourceLocator\Located\LocatedSource;
use Roave\BetterReflection\SourceLocator\Type\SourceLocator;
use function file_exists;

/**
* Use PHP's built in autoloader to locate a class, without actually loading.
*
* There are some prerequisites...
* - we expect the autoloader to load classes from a file (i.e. using require/include)
*
* Modified code from Roave/BetterReflection, Copyright (c) 2017 Roave, LLC.
*/
class AutoloadSourceLocator implements SourceLocator
{

/** @var AstLocator */
private $astLocator;

/**
* Primarily used by the non-loading-autoloader magic trickery to determine
* the filename used during autoloading.
*
* @var string|null
*/
private static $autoloadLocatedFile;

/** @var AstLocator|null */
private static $currentAstLocator;

/**
* Note: the constructor has been made a 0-argument constructor because `\stream_wrapper_register`
* is a piece of trash, and doesn't accept instances, just class names.
*/
public function __construct(?AstLocator $astLocator = null)
{
$validLocator = $astLocator ?? self::$currentAstLocator;
if ($validLocator === null) {
throw new \PHPStan\ShouldNotHappenException();
}

$this->astLocator = $validLocator;
}

/**
* {@inheritDoc}
*
* @throws ParseToAstFailure
*/
public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection
{
if (!$identifier->isClass()) {
return null;
}

$locateResult = $this->locateClassByName($identifier->getName());
if ($locateResult === null) {
return null;
}
[$potentiallyLocatedFile, $className] = $locateResult;

$locatedSource = new LocatedSource(
FileReader::read($potentiallyLocatedFile),
$potentiallyLocatedFile
);

try {
return $this->astLocator->findReflection($reflector, $locatedSource, new Identifier($className, $identifier->getType()));
} catch (IdentifierNotFound $exception) {
return null;
}
}

public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array
{
return []; // todo
}

/**
* Attempt to locate a class by name.
*
* If class already exists, simply use internal reflection API to get the
* filename and store it.
*
* If class does not exist, we make an assumption that whatever autoloaders
* that are registered will be loading a file. We then override the file://
* protocol stream wrapper to "capture" the filename we expect the class to
* be in, and then restore it. Note that class_exists will cause an error
* that it cannot find the file, so we squelch the errors by overriding the
* error handler temporarily.
*
* @throws ReflectionException
* @return array{string, string}|null
*/
private function locateClassByName(string $className): ?array
{
if (class_exists($className, false) || interface_exists($className, false) || trait_exists($className, false)) {
$reflection = new ReflectionClass($className);
$filename = $reflection->getFileName();

if (!is_string($filename)) {
return null;
}

if (!file_exists($filename)) {
return null;
}

return [$filename, $reflection->getName()];
}

self::$autoloadLocatedFile = null;
self::$currentAstLocator = $this->astLocator; // passing the locator on to the implicitly instantiated `self`
$previousErrorHandler = set_error_handler(static function (): bool {
return true;
});
stream_wrapper_unregister('file');
stream_wrapper_register('file', self::class);
class_exists($className);
stream_wrapper_restore('file');
set_error_handler($previousErrorHandler);

if (self::$autoloadLocatedFile === null) {
return null;
}

return [self::$autoloadLocatedFile, $className];
}

/**
* Our wrapper simply records which file we tried to load and returns
* boolean false indicating failure.
*
* @see https://php.net/manual/en/class.streamwrapper.php
* @see https://php.net/manual/en/streamwrapper.stream-open.php
*
* @param string $path
* @param string $mode
* @param int $options
* @param string $opened_path
*
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
*/
public function stream_open($path, $mode, $options, &$opened_path): bool
{
self::$autoloadLocatedFile = $path;

return false;
}

/**
* url_stat is triggered by calls like "file_exists". The call to "file_exists" must not be overloaded.
* This function restores the original "file" stream, issues a call to "stat" to get the real results,
* and then re-registers the AutoloadSourceLocator stream wrapper.
*
* @see https://php.net/manual/en/class.streamwrapper.php
* @see https://php.net/manual/en/streamwrapper.url-stat.php
*
* @param string $path
* @param int $flags
*
* @return mixed[]|bool
*
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
*/
public function url_stat($path, $flags)
{
stream_wrapper_restore('file');

if (($flags & STREAM_URL_STAT_QUIET) !== 0) {
set_error_handler(static function (): bool {
// Use native error handler
return false;
});
$result = @stat($path);
restore_error_handler();
} else {
$result = stat($path);
}

stream_wrapper_unregister('file');
stream_wrapper_register('file', self::class);

return $result;
}

}
13 changes: 9 additions & 4 deletions src/Reflection/ReflectionProvider/ReflectionProviderFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
use PHPStan\Reflection\BetterReflection\Reflector\MemoizingClassReflector;
use PHPStan\Reflection\BetterReflection\Reflector\MemoizingConstantReflector;
use PHPStan\Reflection\BetterReflection\Reflector\MemoizingFunctionReflector;
use PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Reflection\Runtime\RuntimeReflectionProvider;
use Roave\BetterReflection\Reflector\FunctionReflector;
use Roave\BetterReflection\SourceLocator\Ast\Locator;
use Roave\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber;
use Roave\BetterReflection\SourceLocator\Type\AggregateSourceLocator;
use Roave\BetterReflection\SourceLocator\Type\MemoizingSourceLocator;
use Roave\BetterReflection\SourceLocator\Type\PhpInternalSourceLocator;

Expand Down Expand Up @@ -82,10 +84,13 @@ private function createPhpStormStubsReflectionProvider(): ReflectionProvider
$astLocator = new Locator($this->parser, static function () use (&$functionReflector): FunctionReflector {
return $functionReflector;
});
$sourceLocator = new MemoizingSourceLocator(new PhpInternalSourceLocator(
$astLocator,
$this->phpStormStubsSourceStubber
));
$sourceLocator = new MemoizingSourceLocator(new AggregateSourceLocator([
new AutoloadSourceLocator($astLocator),
new PhpInternalSourceLocator(
$astLocator,
$this->phpStormStubsSourceStubber
),
]));
$classReflector = new MemoizingClassReflector($sourceLocator);
$functionReflector = new MemoizingFunctionReflector($sourceLocator, $classReflector);
$constantReflector = new MemoizingConstantReflector($sourceLocator, $classReflector);
Expand Down
1 change: 1 addition & 0 deletions tests/phpstan-bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@

class_alias(\ReturnTypes\Foo::class, \ReturnTypes\FooAlias::class, true);
class_alias(\TestAccessProperties\FooAccessProperties::class, \TestAccessProperties\FooAccessPropertiesAlias::class, true);
class_exists(\Hoa\Stream::class);

0 comments on commit c0cb3a6

Please sign in to comment.